This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-24774-doc-example-guards in repository https://gitbox.apache.org/repos/asf/camel.git
commit fd12c0941eaf07a648e02fb7e1e80f681c988831 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 16 20:18:30 2026 +0200 CAMEL-24774: docs - three more build-time guards for the documentation examples: the XML examples validate against camel-spring.xsd and camel-xml-io.xsd (a camelContext whole, any other Camel element as the global element it is, so a fragment is judged without an order it never meant), every import org.apache.camel line of a Java example names a class of the source tree, and the YAML route examples of the user manual pass the YAML validator; the examples the first run found are correc [...] Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/catalog/docs/cometd-component.adoc | 4 +- .../apache/camel/catalog/docs/netty-component.adoc | 2 +- .../org/apache/camel/catalog/docs/process-eip.adoc | 10 +- .../apache/camel/catalog/docs/resequence-eip.adoc | 4 +- .../apache/camel/catalog/docs/scatter-gather.adoc | 4 +- .../apache/camel/catalog/docs/seda-component.adoc | 2 +- .../org/apache/camel/catalog/docs/split-eip.adoc | 4 +- .../catalog/docs/univocityCsv-dataformat.adoc | 26 +- .../catalog/docs/univocityFixed-dataformat.adoc | 26 +- .../catalog/docs/univocityTsv-dataformat.adoc | 26 +- .../apache/camel/catalog/docs/unmarshal-eip.adoc | 2 +- .../apache/camel/catalog/docs/xpath-language.adoc | 9 +- .../camel/catalog/DocExamplesJavaImportsTest.java | 200 +++++++++++ .../camel/catalog/DocExamplesXmlSchemaTest.java | 387 +++++++++++++++++++++ .../org/apache/camel/catalog/UserManualPages.java | 74 ++++ .../src/main/docs/cometd-component.adoc | 4 +- .../camel-netty/src/main/docs/netty-component.adoc | 2 +- .../camel-seda/src/main/docs/seda-component.adoc | 2 +- .../src/main/docs/univocityCsv-dataformat.adoc | 26 +- .../src/main/docs/univocityFixed-dataformat.adoc | 26 +- .../src/main/docs/univocityTsv-dataformat.adoc | 26 +- .../camel-xpath/src/main/docs/xpath-language.adoc | 9 +- .../main/docs/modules/eips/pages/process-eip.adoc | 10 +- .../docs/modules/eips/pages/resequence-eip.adoc | 4 +- .../docs/modules/eips/pages/scatter-gather.adoc | 4 +- .../main/docs/modules/eips/pages/split-eip.adoc | 4 +- .../docs/modules/eips/pages/unmarshal-eip.adoc | 2 +- docs/user-manual/modules/ROOT/pages/routes.adoc | 2 +- .../yaml/validator/UserManualDocExamplesTest.java | 161 +++++++++ 29 files changed, 941 insertions(+), 121 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cometd-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cometd-component.adoc index 61116f810e83..3627a3a60d89 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cometd-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cometd-component.adoc @@ -125,14 +125,14 @@ Spring XML:: <camel:keyStore resource="/users/home/server/keystore.jks" password="keystorePassword"/> - </camel:keyManagers> + </camel:trustManagers> </camel:sslContextParameters> <bean id="cometd" class="org.apache.camel.component.cometd.CometdComponent"> <property name="sslContextParameters" ref="sslContextParameters"/> </bean> - <to uri="cometds://127.0.0.1:443/service/test?baseResource=file:./target/test-classes/webapp&timeout=240000&interval=0&maxInterval=30000&multiFrameInterval=1500&jsonCommented=true&logLevel=2"/>... + <to uri="cometds://127.0.0.1:443/service/test?baseResource=file:./target/test-classes/webapp&timeout=240000&interval=0&maxInterval=30000&multiFrameInterval=1500&jsonCommented=true&logLevel=2"/>... ---- ==== diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc index e57dbe9c78e9..02a42edec350 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc @@ -722,7 +722,7 @@ Spring DSL based configuration of endpoint </camel:keyManagers> </camel:sslContextParameters>... ... - <to uri="netty:tcp://0.0.0.0:5150?sync=true&ssl=true&sslContextParameters=#sslContextParameters"/> + <to uri="netty:tcp://0.0.0.0:5150?sync=true&ssl=true&sslContextParameters=#sslContextParameters"/> ... ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/process-eip.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/process-eip.adoc index b295726e20dd..654b09187ac4 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/process-eip.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/process-eip.adoc @@ -110,12 +110,10 @@ Or if you use Spring XML you can create the processor via `<bean>`: <bean id="myProcessor" class="com.foo.MyProcessor"/> <camelContext> - <routes> - <route> - <from uri="activemq:myQueue"/> - <process ref="myProcessor"/> - </route> - </routes> + <route> + <from uri="activemq:myQueue"/> + <process ref="myProcessor"/> + </route> </camelContext> </beans> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resequence-eip.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resequence-eip.adoc index aa8d6cbf6d97..aa9a08b4a160 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resequence-eip.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resequence-eip.adoc @@ -395,8 +395,8 @@ XML:: <route> <from uri="jms:queue:foo"/> <resequence> - <header>JMSPriority</header> <batchConfig reverse="true" allowDuplicates="true" batchTimeout="3000"/> + <header>JMSPriority</header> <to uri="mock:result"/> </resequence> </route> @@ -600,8 +600,8 @@ XML:: <route> <from uri="direct:start"/> <resequence> - <method beanType="com.foo.MyFileNameExpression"/> <streamConfig timeout="2000"/> + <method beanType="com.foo.MyFileNameExpression"/> <to uri="mock:result"/> </resequence> </route> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/scatter-gather.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/scatter-gather.adoc index 23f76a54d49d..abdb24209122 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/scatter-gather.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/scatter-gather.adoc @@ -204,7 +204,7 @@ And are loaded up in Spring XML like this: ._XML-only: Spring XML bean definitions for vendor and aggregation strategy_ [source,xml] ---- -<camelContext> +<beans> <bean id="aggregatorStrategy" class="org.apache.camel.spring.processor.scattergather.LowestQuoteAggregationStrategy"/> @@ -220,7 +220,7 @@ And are loaded up in Spring XML like this: <constructor-arg><value>3</value></constructor-arg> </bean> -</camelContext> +</beans> ---- Each bean is loaded with a different price for beer. When the message is sent to each bean endpoint, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/seda-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/seda-component.adoc index 31b66db65857..79d476a3cb51 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/seda-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/seda-component.adoc @@ -69,7 +69,7 @@ but you can use different implementation, you can reference your own </bean> <!-- ... and later --> -<from>seda:array?queue=#arrayQueue</from> +<from uri="seda:array?queue=#arrayQueue"/> ---- You can also reference a `BlockingQueueFactory` implementation. diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/split-eip.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/split-eip.adoc index a20349d8d861..e64c8ef3748e 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/split-eip.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/split-eip.adoc @@ -1545,6 +1545,8 @@ Spring XML:: + [source,xml] ---- +<bean id="myProcessor" class="com.foo.MyProcessor"/> + <camelContext errorHandlerRef="dlc" xmlns="http://camel.apache.org/schema/spring"> <!-- define error handler as DLC, with use original message enabled --> @@ -1552,8 +1554,6 @@ Spring XML:: <redeliveryPolicy maximumRedeliveries="3" redeliveryDelay="0"/> </errorHandler> - <bean id="myProcessor" class="com.foo.MyProcessor"/> - <route> <from uri="direct:start"/> <to uri="mock:a"/> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityCsv-dataformat.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityCsv-dataformat.adoc index 3104d23980c0..ca4ea17ed3b7 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityCsv-dataformat.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityCsv-dataformat.adoc @@ -81,7 +81,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-csv/> + <univocityCsv/> </marshal> <to uri="mock:result"/> </route> @@ -122,11 +122,11 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-fixed padding="_"> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed padding="_"> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </marshal> <to uri="mock:result"/> </route> @@ -168,7 +168,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-tsv/> + <univocityTsv/> </marshal> <to uri="mock:result"/> </route> @@ -229,7 +229,7 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-csv headerExtractionEnabled="true" asMap="true"/> + <univocityCsv headerExtractionEnabled="true" asMap="true"/> </unmarshal> <to uri="mock:result"/> </route> @@ -272,11 +272,11 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-fixed> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </unmarshal> <to uri="mock:result"/> </route> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityFixed-dataformat.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityFixed-dataformat.adoc index 961adf8efc2c..d9b90b1ed344 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityFixed-dataformat.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityFixed-dataformat.adoc @@ -81,7 +81,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-csv/> + <univocityCsv/> </marshal> <to uri="mock:result"/> </route> @@ -122,11 +122,11 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-fixed padding="_"> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed padding="_"> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </marshal> <to uri="mock:result"/> </route> @@ -168,7 +168,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-tsv/> + <univocityTsv/> </marshal> <to uri="mock:result"/> </route> @@ -224,7 +224,7 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-csv headerExtractionEnabled="true" asMap="true"/> + <univocityCsv headerExtractionEnabled="true" asMap="true"/> </unmarshal> <to uri="mock:result"/> </route> @@ -267,11 +267,11 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-fixed> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </unmarshal> <to uri="mock:result"/> </route> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityTsv-dataformat.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityTsv-dataformat.adoc index a66d3f6331b1..3c015d40de04 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityTsv-dataformat.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/univocityTsv-dataformat.adoc @@ -81,7 +81,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-csv/> + <univocityCsv/> </marshal> <to uri="mock:result"/> </route> @@ -122,11 +122,11 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-fixed padding="_"> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed padding="_"> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </marshal> <to uri="mock:result"/> </route> @@ -168,7 +168,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-tsv/> + <univocityTsv/> </marshal> <to uri="mock:result"/> </route> @@ -224,7 +224,7 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-csv headerExtractionEnabled="true" asMap="true"/> + <univocityCsv headerExtractionEnabled="true" asMap="true"/> </unmarshal> <to uri="mock:result"/> </route> @@ -267,11 +267,11 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-fixed> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </unmarshal> <to uri="mock:result"/> </route> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/unmarshal-eip.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/unmarshal-eip.adoc index 572d60d9609c..a1ef60ca7241 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/unmarshal-eip.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/unmarshal-eip.adoc @@ -105,7 +105,7 @@ XML:: <route> <from uri="file:inbox/xml"/> <unmarshal allowNullBody="true"> - <jaxb/> + <jaxb contextPath="com.acme.model"/> </unmarshal> <to uri="bean:validateOrder"/> </route> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xpath-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xpath-language.adoc index b539d1cd17d9..1dbf51f48015 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xpath-language.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xpath-language.adoc @@ -398,20 +398,21 @@ type, so the concat function works. == Using XPath on Headers Some users may have XML stored in a header. To apply an XPath to a -header's value, you can do this by defining the 'headerName' attribute. +header's value, you can do this by defining the `source` attribute with the `header:` prefix +(`variable:` and `property:` work the same way for an exchange variable or property). [source,xml] ---- -<xpath headerName="invoiceDetails">/invoice/@orderType = 'premium'</xpath> +<xpath source="header:invoiceDetails">/invoice/@orderType = 'premium'</xpath> ---- -And in Java DSL you specify the headerName as the second parameter as shown: +And in Java DSL you specify the source with the fluent expression builder as shown: ._Java-only: XPath expression builder API_ [source,java] ---- -xpath("/invoice/@orderType = 'premium'", "invoiceDetails") +expression().xpath("/invoice/@orderType = 'premium'").source("header:invoiceDetails").end() ---- == Example diff --git a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesJavaImportsTest.java b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesJavaImportsTest.java new file mode 100644 index 000000000000..efb28e469652 --- /dev/null +++ b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesJavaImportsTest.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.catalog; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Every {@code import org.apache.camel...} line of a Java example in the documentation names a class of the source tree + * (CAMEL-24774): an import of a class that moved to another package, or never was in the one written, fails the build + * here instead of in the reader's IDE. The examples of the catalog pages and of the user manual are checked when the + * tests run inside the Camel repository, where the source tree is. + */ +class DocExamplesJavaImportsTest { + + private static final Pattern JAVA_BLOCK = Pattern.compile("\\[source,java\\]\\n-{4,}\\n(.*?)\\n-{4,}", Pattern.DOTALL); + private static final Pattern IMPORT + = Pattern.compile("^\\s*import\\s+(static\\s+)?(org\\.apache\\.camel\\.[\\w.]+?)(\\.\\*)?\\s*;", Pattern.MULTILINE); + + private static CamelCatalog catalog; + private static Path root; + private static Set<String> classes; + + private record DocExamples(int imports, List<String> failures) { + } + + @BeforeAll + static void setup() throws Exception { + catalog = new DefaultCamelCatalog(); + root = UserManualPages.repositoryRoot(); + if (root != null) { + classes = classesOfTheSourceTree(root); + } + } + + @Test + void everyImportOfTheDocumentationExamplesResolves() throws Exception { + assumeTrue(root != null, "the imports are only checked inside the Camel source tree"); + Map<String, String> pages = new LinkedHashMap<>(); + for (String page : catalog.findDocNames()) { + String doc = catalog.asciiDoc(page); + if (doc != null) { + pages.put(page, doc); + } + } + + DocExamples result = check(pages); + + assertTrue(result.imports() > 50, "imports found in the documentation examples: " + result.imports()); + assertTrue(result.failures().isEmpty(), + "Documentation examples importing a class that is not there:\n " + String.join("\n ", result.failures())); + } + + @Test + void everyImportOfTheUserManualExamplesResolves() throws Exception { + assumeTrue(root != null, "the imports are only checked inside the Camel source tree"); + + DocExamples result = check(UserManualPages.currentPages()); + + assertTrue(result.imports() > 20, "imports found in the user manual examples: " + result.imports()); + assertTrue(result.failures().isEmpty(), + "User manual examples importing a class that is not there:\n " + String.join("\n ", result.failures())); + } + + @Test + void theCheckSeesWhatItIsFor() { + assumeTrue(root != null, "the imports are only checked inside the Camel source tree"); + String page = "[source,java]\n----\n" + + "import org.apache.camel.CamelContext;\n" + + "import org.apache.camel.builder.RouteBuilder;\n" + + "import org.apache.camel.model.OnExceptionDefinition.RedeliveryOption;\n" + + "import org.apache.camel.support.builder.*;\n" + + "import static org.apache.camel.builder.PredicateBuilder.not;\n" + + "import org.apache.camel.util.jsse.SSLContextParameters;\n" + + "import org.apache.camel.impl.DefaultProducer;\n" + + "----\n"; + + DocExamples result = check(Map.of("fake", page)); + + assertTrue(result.imports() == 7, "imports seen: " + result.imports()); + assertTrue(result.failures().size() == 2 && result.failures().get(0).contains("org.apache.camel.util.jsse") + && result.failures().get(1).contains("org.apache.camel.impl.DefaultProducer"), + "the two classes that moved, not the nested class, the static member or the wildcard: " + result.failures()); + } + + private static DocExamples check(Map<String, String> pages) { + int imports = 0; + List<String> failures = new ArrayList<>(); + for (Map.Entry<String, String> entry : pages.entrySet()) { + String page = entry.getKey(); + String doc = entry.getValue(); + Matcher m = JAVA_BLOCK.matcher(doc); + while (m.find()) { + String java = m.group(1); + Matcher im = IMPORT.matcher(java); + while (im.find()) { + imports++; + String name = im.group(2); + boolean wildcard = im.group(3) != null; + if (!resolves(name, im.group(1) != null, wildcard)) { + int line = 1 + countLines(doc, m.start(1)) + countLines(java, im.start()); + failures.add(page + ".adoc:" + line + ": import " + name + (wildcard ? ".*" : "")); + } + } + } + } + return new DocExamples(imports, failures); + } + + /** + * Whether the imported name is a class of the source tree, a nested class of one, a static member of one, or a + * package or class a wildcard import refers to. + */ + private static boolean resolves(String name, boolean isStatic, boolean wildcard) { + if (wildcard) { + if (classes.contains(name)) { + return true; + } + String prefix = name + "."; + return classes.stream().anyMatch(c -> c.startsWith(prefix)); + } + // the name itself, then the outer classes of a nested class or the class of a static member + String candidate = name; + while (true) { + if (classes.contains(candidate)) { + return true; + } + int dot = candidate.lastIndexOf('.'); + if (dot < 0) { + return false; + } + String last = candidate.substring(dot + 1); + candidate = candidate.substring(0, dot); + // a lower-case segment is a package, not a class that could nest the rest + if (!isStatic && !Character.isUpperCase(last.charAt(0))) { + return false; + } + } + } + + /** The fully qualified names of the classes of the source tree, from src/main/java and src/generated/java. */ + private static Set<String> classesOfTheSourceTree(Path root) throws IOException { + Set<String> answer = new HashSet<>(); + try (Stream<Path> files = Files.walk(root)) { + files.filter(f -> f.toString().endsWith(".java")) + .map(Path::toString) + .forEach(f -> { + int i = f.indexOf("/src/main/java/org/apache/camel/"); + if (i < 0) { + i = f.indexOf("/src/generated/java/org/apache/camel/"); + } + if (i >= 0 && !f.contains("/target/")) { + String rel = f.substring(f.indexOf("/org/apache/camel/", i) + 1, f.length() - 5); + answer.add(rel.replace('/', '.')); + } + }); + } + return answer; + } + + private static int countLines(String text, int end) { + int n = 0; + for (int i = 0; i < end; i++) { + if (text.charAt(i) == '\n') { + n++; + } + } + return n; + } +} diff --git a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java new file mode 100644 index 000000000000..7049edd30cc9 --- /dev/null +++ b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java @@ -0,0 +1,387 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.catalog; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXParseException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * The XML examples of the documentation bundled in the catalog must validate against the XML schemas bundled next to + * them (CAMEL-24774): a {@code <camelContext>} against camel-spring.xsd as Spring XML validates it, the xml-io roots + * ({@code <camel>}, {@code <routes>}...) against camel-xml-io.xsd, and any other Camel element written on its own + * ({@code <route>}, {@code <onException>}, {@code <marshal>}...) as the global element it is in camel-spring.xsd, so a + * fragment is checked without a wrapper imposing an element order the page never meant. + * <p/> + * The examples are what people copy: a Camel 1.x namespace, an attribute renamed in Camel 3, a tag closed by another + * tag or a {@code &} in a URI fails the build here instead of in the reader's application. + */ +class DocExamplesXmlSchemaTest { + + private static final Pattern XML_BLOCK = Pattern.compile("\\[source,xml\\]\\n-{4,}\\n(.*?)\\n-{4,}", Pattern.DOTALL); + private static final Pattern CALLOUT = Pattern.compile("\\s*<\\d+>\\s*$", Pattern.MULTILINE); + private static final Pattern XML_DECLARATION = Pattern.compile("<\\?xml[^>]*\\?>"); + private static final Pattern CAMEL_CONTEXT + = Pattern.compile("<(\\w+:)?camelContext\\b.*?</(\\w+:)?camelContext>", Pattern.DOTALL); + /** + * An element that makes a block a Camel example rather than a payload, a Maven POM, an XSLT or a configuration file + * of another project which happens to share element names with the Camel schema. + */ + private static final Pattern CAMEL_SIGNATURE = Pattern.compile("<(camelContext|routes|route|from|to|toD|" + + "marshal|unmarshal|dataFormats|rest|restConfiguration|" + + "routeTemplate|templatedRoute|routeConfiguration|" + + "onException|errorHandler|intercept|interceptFrom|" + + "interceptSendToEndpoint|onCompletion|setHeader|setBody|" + + "setProperty|setVariable|choice|filter|split|aggregate|" + + "multicast|recipientList|wireTap|log|process|transform|" + + "convertBodyTo|validate|enrich|pollEnrich|threads|threadPool|" + + "propertyPlaceholder|globalOptions|streamCaching|jmxAgent|" + + "sslContextParameters|xpath|simple|csimple|jsonpath|jq|" + + "xtokenize|tokenize|xquery|groovy|datasonnet|mvel|ognl)" + + "[\\s/>]"); + + private static final String SPRING_NS = "http://camel.apache.org/schema/spring"; + private static final String XML_IO_NS = "http://camel.apache.org/schema/xml-io"; + private static final String SPRING_BEANS_NS = "http://www.springframework.org/schema/beans"; + + /** The roots of an xml-io file, validated against camel-xml-io.xsd. */ + private static final Set<String> XML_IO_ROOTS + = Set.of("camel", "routes", "rests", "routeTemplates", "templatedRoutes", "routeConfigurations"); + + /** + * Top-level elements that are not judged on their own: a Spring {@code <bean>} and its {@code <property>} share + * their names with Camel elements, and the rest are Maven, XSLT or payload samples. + */ + private static final Set<String> IGNORED_ROOTS = Set.of("bean", "beans", "property", "dependency", "dependencies", + "dependencyManagement", "plugin", "plugins", "build", "project", "extensions"); + + /** + * Examples the schemas cannot judge, by page and a text found in the example: an elided {@code <beans ...>} start + * tag, a Spring bean next to route fragments, and the {@code <namespace>} child of an expression, which the model + * and the xml-io parser support but no generated schema can express next to the expression text (JAXB + * {@code @XmlValue}), so Spring XML declares the namespaces as {@code xmlns:} attributes instead; and the endpoint + * page's unescaped {@code &}, which is there to show the error it causes. + */ + private static final Map<String, String> EXAMPLES_SKIPPED = Map.of( + "xmlsecurity-sign-component", "<bean id=\"xadesProperties\"", + "xmlsecurity-verify-component", "<bean id=\"xadesProperties\"", + "spring-summary", "<beans xmlns=\"http://www.springframework.org/schema/beans\"", + "split-eip", "<namespace key=", + "xtokenize-language", "<namespace key=", + "endpoint", "paramA=1¶mB=2"); + + private static CamelCatalog catalog; + private static Schema springSchema; + private static Schema xmlIoSchema; + private static Set<String> springGlobalElements; + + private record DocExamples(int examples, List<String> failures) { + } + + @BeforeAll + static void setup() throws Exception { + catalog = new DefaultCamelCatalog(); + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + springSchema = factory.newSchema(new StreamSource(new StringReader(catalog.springSchemaAsXml()))); + xmlIoSchema = factory.newSchema(new StreamSource(new StringReader(catalog.xmlIoSchemaAsXml()))); + springGlobalElements = globalElements(catalog.springSchemaAsXml()); + } + + @Test + void everyXmlExampleOfTheDocumentationValidates() throws Exception { + List<String> pages = new ArrayList<>(catalog.findDocNames()); + + DocExamples result = validate(pages); + + assertTrue(result.examples() > 1500, "XML examples found in the documentation: " + result.examples()); + assertTrue(result.failures().isEmpty(), + "Documentation XML examples that do not validate:\n " + String.join("\n ", result.failures())); + } + + @Test + void everyXmlExampleOfTheUserManualValidates() throws Exception { + Map<String, String> pages = UserManualPages.currentPages(); + assumeTrue(!pages.isEmpty(), "the user manual is only checked inside the Camel source tree"); + + DocExamples result = validate(pages); + + assertTrue(result.examples() > 200, "XML examples found in the user manual: " + result.examples()); + assertTrue(result.failures().isEmpty(), + "User manual XML examples that do not validate:\n " + String.join("\n ", result.failures())); + } + + @Test + void theCheckSeesWhatItIsFor() { + // the classes of mistakes the check exists for + assertTrue(validateBlock("<camelContext xmlns=\"http://activemq.apache.org/camel/schema/spring\">\n" + + "<route><from uri=\"direct:a\"/><to uri=\"mock:b\"/></route></camelContext>") + .toString().contains("camelContext"), "the Camel 1.x namespace"); + assertTrue(!validateBlock("<from>seda:a?size=1</from>").isEmpty(), "a from with text content"); + assertTrue(!validateBlock("<onCompletion executorServiceRef=\"x\"><to uri=\"mock:a\"/></onCompletion>").isEmpty(), + "a Camel 2 attribute"); + assertTrue(!validateBlock("<route><from uri=\"direct:a\"/><to uri=\"mock:b?x=1&y=2\"/></route>").isEmpty(), + "an unescaped ampersand"); + assertTrue(!validateBlock("<route><from uri=\"direct:a\"/><marshal><jaxb/></marshal></route>").isEmpty(), + "a missing required attribute"); + assertTrue(!validateBlock("<onException><redeliveryPolicy maximumRedeliveries=\"1\"/>" + + "<exception>java.io.IOException</exception></onException>") + .isEmpty(), + "the element order Spring XML enforces"); + // and what it leaves alone + assertTrue(validateBlock("<route id=\"a\"><from uri=\"direct:a\"/><to uri=\"mock:b\"/></route>\n" + + "<onException><exception>java.io.IOException</exception><to uri=\"mock:e\"/></onException>") + .isEmpty(), "valid fragments in any order"); + assertTrue(validateBlock("<routes><route><from uri=\"direct:a\"/><to uri=\"mock:b\"/></route></routes>").isEmpty(), + "an xml-io root"); + assertTrue(validateBlock("<mapper><delete id=\"x\" parameterType=\"int\">delete from t</delete></mapper>").isEmpty(), + "a MyBatis mapper sharing element names"); + assertTrue(validateBlock("<bean id=\"a\" class=\"com.foo.A\"/>\n<from uri=\"direct:a\"/> <1>\n...").isEmpty(), + "a Spring bean, a callout and a placeholder line"); + } + + @Test + void theSkippedPagesStillExist() throws Exception { + Map<String, String> manual = UserManualPages.pages(); + for (String page : EXAMPLES_SKIPPED.keySet()) { + assertTrue(catalog.asciiDoc(page) != null || manual.containsKey(page), + "the skipped page " + page + " is gone, drop the entry"); + } + } + + private static DocExamples validate(List<String> pages) throws Exception { + Map<String, String> docs = new LinkedHashMap<>(); + for (String page : pages) { + String doc = catalog.asciiDoc(page); + if (doc != null) { + docs.put(page, doc); + } + } + return validate(docs); + } + + private static DocExamples validate(Map<String, String> pages) { + int examples = 0; + List<String> failures = new ArrayList<>(); + for (Map.Entry<String, String> entry : pages.entrySet()) { + String page = entry.getKey(); + String doc = entry.getValue(); + int n = 0; + Matcher m = XML_BLOCK.matcher(doc); + while (m.find()) { + n++; + String xml = m.group(1); + String skipped = EXAMPLES_SKIPPED.get(page); + if (skipped != null && xml.contains(skipped)) { + continue; + } + int line = 1 + countLines(doc, m.start(1)); + for (String error : validateBlock(xml)) { + failures.add(page + ".adoc:" + line + " example " + n + ": " + error); + } + examples++; + } + } + return new DocExamples(examples, failures); + } + + /** + * Validates one XML example and returns what is wrong with it (empty when it validates or is not an example the + * schemas can judge). + */ + static List<String> validateBlock(String xml) { + List<String> errors = new ArrayList<>(); + String text = clean(xml); + if (text.isBlank() || !CAMEL_SIGNATURE.matcher(text).find()) { + // a payload, a POM, an XSLT: not something the Camel schemas describe + return errors; + } + if (text.contains("<camelContext") || text.contains(":camelContext")) { + Matcher cm = CAMEL_CONTEXT.matcher(text); + while (cm.find()) { + String context = cm.group(0); + if (cm.group(1) != null) { + String prefix = cm.group(1).substring(0, cm.group(1).length() - 1); + if (!context.contains("xmlns:" + prefix + "=")) { + context = context.replaceFirst("<" + prefix + ":camelContext", + "<" + prefix + ":camelContext xmlns:" + prefix + "=\"" + SPRING_NS + "\""); + } + } else { + context = withDefaultNamespace(context, "camelContext", SPRING_NS); + } + validate(context, springSchema, errors); + } + return errors; + } + + List<Element> roots; + try { + roots = topLevelElements(text); + } catch (SAXParseException e) { + errors.add("not well-formed XML: " + e.getMessage()); + return errors; + } catch (Exception e) { + errors.add("not well-formed XML: " + e); + return errors; + } + for (Element root : roots) { + String name = root.getNodeName(); + String local = name.contains(":") ? name.substring(name.indexOf(':') + 1) : name; + String ns = root.getAttribute("xmlns"); + if (SPRING_BEANS_NS.equals(ns) || IGNORED_ROOTS.contains(local) || name.contains(":")) { + // a Spring beans file, a Maven or payload sample, or a prefixed element of another schema + continue; + } + String element = serialize(root); + if (element == null) { + // an undeclared prefix (prop:, camel:...) the page declares elsewhere, nothing to judge + continue; + } + if (XML_IO_ROOTS.contains(local)) { + validate(withDefaultNamespace(element, local, XML_IO_NS), xmlIoSchema, errors); + } else if (springGlobalElements.contains(local)) { + validate(withDefaultNamespace(element, local, SPRING_NS), springSchema, errors); + } + } + return errors; + } + + private static void validate(String document, Schema schema, List<String> errors) { + try { + Validator validator = schema.newValidator(); + validator.validate(new StreamSource(new StringReader(document))); + } catch (SAXParseException e) { + String msg = e.getMessage(); + if (msg != null && msg.contains("is not bound")) { + // a fragment using a prefix its page declares elsewhere (camel:, prop:, xsl:...), nothing to judge + return; + } + errors.add(msg); + } catch (Exception e) { + errors.add(e.toString()); + } + } + + /** Drops the XML declaration, the placeholder lines and the AsciiDoc callouts, which are not part of the XML. */ + static String clean(String xml) { + String text = XML_DECLARATION.matcher(xml).replaceAll(""); + text = CALLOUT.matcher(text).replaceAll(""); + StringBuilder sb = new StringBuilder(); + for (String line : text.split("\n")) { + String t = line.trim(); + if (t.equals("...") || t.equals("…")) { + continue; + } + sb.append(line).append('\n'); + } + return sb.toString(); + } + + /** Adds the default namespace to the root element when the example does not declare one. */ + private static String withDefaultNamespace(String document, String root, String ns) { + int end = document.indexOf('>'); + String startTag = end > 0 ? document.substring(0, end) : document; + if (startTag.contains("xmlns=")) { + return document; + } + return document.replaceFirst("<" + root + "\\b", "<" + root + " xmlns=\"" + ns + "\""); + } + + /** The top-level elements of a fragment, parsed without namespaces so undeclared prefixes do not matter. */ + private static List<Element> topLevelElements(String fragment) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document dom = builder.parse(new InputSource(new StringReader("<wrap>" + fragment + "</wrap>"))); + List<Element> answer = new ArrayList<>(); + NodeList children = dom.getDocumentElement().getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + answer.add((Element) child); + } + } + return answer; + } + + private static String serialize(Element element) { + try { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + StringWriter sw = new StringWriter(); + transformer.transform(new DOMSource(element), new StreamResult(sw)); + return sw.toString(); + } catch (Exception e) { + // an attribute with a prefix the fragment does not declare cannot be serialized + return null; + } + } + + /** The names of the global elements of a schema: the elements an example can start with. */ + private static Set<String> globalElements(String xsd) { + Set<String> answer = new HashSet<>(); + Matcher m = Pattern.compile("\n <xs:element name=\"([A-Za-z-]+)\"").matcher(xsd); + while (m.find()) { + answer.add(m.group(1)); + } + return answer; + } + + private static int countLines(String text, int end) { + int n = 0; + for (int i = 0; i < end; i++) { + if (text.charAt(i) == '\n') { + n++; + } + } + return n; + } +} diff --git a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/UserManualPages.java b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/UserManualPages.java new file mode 100644 index 000000000000..ff02be565265 --- /dev/null +++ b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/UserManualPages.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.catalog; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Stream; + +/** + * The pages of the user manual, read from the source tree when the tests run inside the Camel repository (the manual is + * not bundled in the catalog, unlike the component, data format, language and EIP pages). + */ +final class UserManualPages { + + private static final Path PAGES = Path.of("docs", "user-manual", "modules", "ROOT", "pages"); + + private UserManualPages() { + } + + /** The root of the Camel source tree, or null when the tests do not run inside it. */ + static Path repositoryRoot() { + Path dir = Path.of("").toAbsolutePath(); + while (dir != null) { + if (Files.isDirectory(dir.resolve(PAGES)) && Files.isDirectory(dir.resolve("components"))) { + return dir; + } + dir = dir.getParent(); + } + return null; + } + + /** + * The pages that document the current release, by name: the upgrade and migration guides are left out, as they show + * the syntax of older releases on purpose. + */ + static Map<String, String> currentPages() throws IOException { + Map<String, String> answer = pages(); + answer.keySet().removeIf(name -> name.contains("upgrade-guide") || name.contains("migration-guide")); + return answer; + } + + /** The pages by name (the file name without .adoc), or an empty map outside the source tree. */ + static Map<String, String> pages() throws IOException { + Map<String, String> answer = new TreeMap<>(); + Path root = repositoryRoot(); + if (root == null) { + return answer; + } + try (Stream<Path> files = Files.walk(root.resolve(PAGES))) { + for (Path file : files.filter(f -> f.toString().endsWith(".adoc")).toList()) { + String name = file.getFileName().toString(); + answer.put(name.substring(0, name.length() - 5), Files.readString(file)); + } + } + return answer; + } +} diff --git a/components/camel-cometd/src/main/docs/cometd-component.adoc b/components/camel-cometd/src/main/docs/cometd-component.adoc index 61116f810e83..3627a3a60d89 100644 --- a/components/camel-cometd/src/main/docs/cometd-component.adoc +++ b/components/camel-cometd/src/main/docs/cometd-component.adoc @@ -125,14 +125,14 @@ Spring XML:: <camel:keyStore resource="/users/home/server/keystore.jks" password="keystorePassword"/> - </camel:keyManagers> + </camel:trustManagers> </camel:sslContextParameters> <bean id="cometd" class="org.apache.camel.component.cometd.CometdComponent"> <property name="sslContextParameters" ref="sslContextParameters"/> </bean> - <to uri="cometds://127.0.0.1:443/service/test?baseResource=file:./target/test-classes/webapp&timeout=240000&interval=0&maxInterval=30000&multiFrameInterval=1500&jsonCommented=true&logLevel=2"/>... + <to uri="cometds://127.0.0.1:443/service/test?baseResource=file:./target/test-classes/webapp&timeout=240000&interval=0&maxInterval=30000&multiFrameInterval=1500&jsonCommented=true&logLevel=2"/>... ---- ==== diff --git a/components/camel-netty/src/main/docs/netty-component.adoc b/components/camel-netty/src/main/docs/netty-component.adoc index e57dbe9c78e9..02a42edec350 100644 --- a/components/camel-netty/src/main/docs/netty-component.adoc +++ b/components/camel-netty/src/main/docs/netty-component.adoc @@ -722,7 +722,7 @@ Spring DSL based configuration of endpoint </camel:keyManagers> </camel:sslContextParameters>... ... - <to uri="netty:tcp://0.0.0.0:5150?sync=true&ssl=true&sslContextParameters=#sslContextParameters"/> + <to uri="netty:tcp://0.0.0.0:5150?sync=true&ssl=true&sslContextParameters=#sslContextParameters"/> ... ---- diff --git a/components/camel-seda/src/main/docs/seda-component.adoc b/components/camel-seda/src/main/docs/seda-component.adoc index 31b66db65857..79d476a3cb51 100644 --- a/components/camel-seda/src/main/docs/seda-component.adoc +++ b/components/camel-seda/src/main/docs/seda-component.adoc @@ -69,7 +69,7 @@ but you can use different implementation, you can reference your own </bean> <!-- ... and later --> -<from>seda:array?queue=#arrayQueue</from> +<from uri="seda:array?queue=#arrayQueue"/> ---- You can also reference a `BlockingQueueFactory` implementation. diff --git a/components/camel-univocity-parsers/src/main/docs/univocityCsv-dataformat.adoc b/components/camel-univocity-parsers/src/main/docs/univocityCsv-dataformat.adoc index 3104d23980c0..ca4ea17ed3b7 100644 --- a/components/camel-univocity-parsers/src/main/docs/univocityCsv-dataformat.adoc +++ b/components/camel-univocity-parsers/src/main/docs/univocityCsv-dataformat.adoc @@ -81,7 +81,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-csv/> + <univocityCsv/> </marshal> <to uri="mock:result"/> </route> @@ -122,11 +122,11 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-fixed padding="_"> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed padding="_"> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </marshal> <to uri="mock:result"/> </route> @@ -168,7 +168,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-tsv/> + <univocityTsv/> </marshal> <to uri="mock:result"/> </route> @@ -229,7 +229,7 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-csv headerExtractionEnabled="true" asMap="true"/> + <univocityCsv headerExtractionEnabled="true" asMap="true"/> </unmarshal> <to uri="mock:result"/> </route> @@ -272,11 +272,11 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-fixed> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </unmarshal> <to uri="mock:result"/> </route> diff --git a/components/camel-univocity-parsers/src/main/docs/univocityFixed-dataformat.adoc b/components/camel-univocity-parsers/src/main/docs/univocityFixed-dataformat.adoc index 961adf8efc2c..d9b90b1ed344 100644 --- a/components/camel-univocity-parsers/src/main/docs/univocityFixed-dataformat.adoc +++ b/components/camel-univocity-parsers/src/main/docs/univocityFixed-dataformat.adoc @@ -81,7 +81,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-csv/> + <univocityCsv/> </marshal> <to uri="mock:result"/> </route> @@ -122,11 +122,11 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-fixed padding="_"> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed padding="_"> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </marshal> <to uri="mock:result"/> </route> @@ -168,7 +168,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-tsv/> + <univocityTsv/> </marshal> <to uri="mock:result"/> </route> @@ -224,7 +224,7 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-csv headerExtractionEnabled="true" asMap="true"/> + <univocityCsv headerExtractionEnabled="true" asMap="true"/> </unmarshal> <to uri="mock:result"/> </route> @@ -267,11 +267,11 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-fixed> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </unmarshal> <to uri="mock:result"/> </route> diff --git a/components/camel-univocity-parsers/src/main/docs/univocityTsv-dataformat.adoc b/components/camel-univocity-parsers/src/main/docs/univocityTsv-dataformat.adoc index a66d3f6331b1..3c015d40de04 100644 --- a/components/camel-univocity-parsers/src/main/docs/univocityTsv-dataformat.adoc +++ b/components/camel-univocity-parsers/src/main/docs/univocityTsv-dataformat.adoc @@ -81,7 +81,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-csv/> + <univocityCsv/> </marshal> <to uri="mock:result"/> </route> @@ -122,11 +122,11 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-fixed padding="_"> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed padding="_"> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </marshal> <to uri="mock:result"/> </route> @@ -168,7 +168,7 @@ XML:: <route> <from uri="direct:input"/> <marshal> - <univocity-tsv/> + <univocityTsv/> </marshal> <to uri="mock:result"/> </route> @@ -224,7 +224,7 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-csv headerExtractionEnabled="true" asMap="true"/> + <univocityCsv headerExtractionEnabled="true" asMap="true"/> </unmarshal> <to uri="mock:result"/> </route> @@ -267,11 +267,11 @@ XML:: <route> <from uri="direct:input"/> <unmarshal> - <univocity-fixed> - <univocity-header length="5"/> - <univocity-header length="5"/> - <univocity-header length="5"/> - </univocity-fixed> + <univocityFixed> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + <univocityHeader length="5"/> + </univocityFixed> </unmarshal> <to uri="mock:result"/> </route> diff --git a/components/camel-xpath/src/main/docs/xpath-language.adoc b/components/camel-xpath/src/main/docs/xpath-language.adoc index b539d1cd17d9..1dbf51f48015 100644 --- a/components/camel-xpath/src/main/docs/xpath-language.adoc +++ b/components/camel-xpath/src/main/docs/xpath-language.adoc @@ -398,20 +398,21 @@ type, so the concat function works. == Using XPath on Headers Some users may have XML stored in a header. To apply an XPath to a -header's value, you can do this by defining the 'headerName' attribute. +header's value, you can do this by defining the `source` attribute with the `header:` prefix +(`variable:` and `property:` work the same way for an exchange variable or property). [source,xml] ---- -<xpath headerName="invoiceDetails">/invoice/@orderType = 'premium'</xpath> +<xpath source="header:invoiceDetails">/invoice/@orderType = 'premium'</xpath> ---- -And in Java DSL you specify the headerName as the second parameter as shown: +And in Java DSL you specify the source with the fluent expression builder as shown: ._Java-only: XPath expression builder API_ [source,java] ---- -xpath("/invoice/@orderType = 'premium'", "invoiceDetails") +expression().xpath("/invoice/@orderType = 'premium'").source("header:invoiceDetails").end() ---- == Example diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/process-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/process-eip.adoc index b295726e20dd..654b09187ac4 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/process-eip.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/process-eip.adoc @@ -110,12 +110,10 @@ Or if you use Spring XML you can create the processor via `<bean>`: <bean id="myProcessor" class="com.foo.MyProcessor"/> <camelContext> - <routes> - <route> - <from uri="activemq:myQueue"/> - <process ref="myProcessor"/> - </route> - </routes> + <route> + <from uri="activemq:myQueue"/> + <process ref="myProcessor"/> + </route> </camelContext> </beans> diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/resequence-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/resequence-eip.adoc index aa8d6cbf6d97..aa9a08b4a160 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/resequence-eip.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/resequence-eip.adoc @@ -395,8 +395,8 @@ XML:: <route> <from uri="jms:queue:foo"/> <resequence> - <header>JMSPriority</header> <batchConfig reverse="true" allowDuplicates="true" batchTimeout="3000"/> + <header>JMSPriority</header> <to uri="mock:result"/> </resequence> </route> @@ -600,8 +600,8 @@ XML:: <route> <from uri="direct:start"/> <resequence> - <method beanType="com.foo.MyFileNameExpression"/> <streamConfig timeout="2000"/> + <method beanType="com.foo.MyFileNameExpression"/> <to uri="mock:result"/> </resequence> </route> diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/scatter-gather.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/scatter-gather.adoc index 23f76a54d49d..abdb24209122 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/scatter-gather.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/scatter-gather.adoc @@ -204,7 +204,7 @@ And are loaded up in Spring XML like this: ._XML-only: Spring XML bean definitions for vendor and aggregation strategy_ [source,xml] ---- -<camelContext> +<beans> <bean id="aggregatorStrategy" class="org.apache.camel.spring.processor.scattergather.LowestQuoteAggregationStrategy"/> @@ -220,7 +220,7 @@ And are loaded up in Spring XML like this: <constructor-arg><value>3</value></constructor-arg> </bean> -</camelContext> +</beans> ---- Each bean is loaded with a different price for beer. When the message is sent to each bean endpoint, diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/split-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/split-eip.adoc index a20349d8d861..e64c8ef3748e 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/split-eip.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/split-eip.adoc @@ -1545,6 +1545,8 @@ Spring XML:: + [source,xml] ---- +<bean id="myProcessor" class="com.foo.MyProcessor"/> + <camelContext errorHandlerRef="dlc" xmlns="http://camel.apache.org/schema/spring"> <!-- define error handler as DLC, with use original message enabled --> @@ -1552,8 +1554,6 @@ Spring XML:: <redeliveryPolicy maximumRedeliveries="3" redeliveryDelay="0"/> </errorHandler> - <bean id="myProcessor" class="com.foo.MyProcessor"/> - <route> <from uri="direct:start"/> <to uri="mock:a"/> diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/unmarshal-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/unmarshal-eip.adoc index 572d60d9609c..a1ef60ca7241 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/unmarshal-eip.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/unmarshal-eip.adoc @@ -105,7 +105,7 @@ XML:: <route> <from uri="file:inbox/xml"/> <unmarshal allowNullBody="true"> - <jaxb/> + <jaxb contextPath="com.acme.model"/> </unmarshal> <to uri="bean:validateOrder"/> </route> diff --git a/docs/user-manual/modules/ROOT/pages/routes.adoc b/docs/user-manual/modules/ROOT/pages/routes.adoc index a2398252b001..eac3d85f8de7 100644 --- a/docs/user-manual/modules/ROOT/pages/routes.adoc +++ b/docs/user-manual/modules/ROOT/pages/routes.adoc @@ -237,7 +237,7 @@ XML:: ---- <route precondition="'{{format}}' == 'xml'"> <from uri="direct:in"/> - <unmarshal><jaxb/></unmarshal> + <unmarshal><jaxb contextPath="com.foo.model"/></unmarshal> <to uri="direct:out"/> </route> ---- diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/UserManualDocExamplesTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/UserManualDocExamplesTest.java new file mode 100644 index 000000000000..ce20dfc48faf --- /dev/null +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/UserManualDocExamplesTest.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.yaml.validator; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.Error; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * The YAML route examples of the user manual must pass the YAML validator (CAMEL-24774), as the examples of the pages + * bundled in the catalog do in {@link EipDocExamplesTest}: the manual is not in the catalog, so its pages are read from + * the source tree when the tests run inside the Camel repository. + * <p/> + * A block is a route example when its first entry is one of the roots of the YAML DSL schema ({@code - route:}, + * {@code - beans:}, {@code - rest:}...); a step written on its own ({@code - setBody:}) or the YAML of another tool is + * not judged. The upgrade and migration guides show the syntax of older releases on purpose and are left out. + */ +class UserManualDocExamplesTest { + + private static final Path PAGES = Path.of("docs", "user-manual", "modules", "ROOT", "pages"); + private static final Pattern YAML_BLOCK = Pattern.compile("\\[source,yaml\\]\\n-{4}\\n(.*?)\\n-{4}", Pattern.DOTALL); + private static final Pattern FIRST_KEY = Pattern.compile("^\\s*-\\s+([A-Za-z]+)\\s*:"); + + private static YamlValidator validator; + private static Set<String> roots; + + private record DocExamples(int examples, List<String> failures) { + } + + @BeforeAll + static void setup() throws Exception { + validator = new YamlValidator(); + validator.init(); + roots = new HashSet<>(); + try (InputStream is = YamlValidator.class.getResourceAsStream("/schema/camelYamlDsl.json")) { + JsonNode schema = new ObjectMapper().readTree(is); + schema.path("items").path("properties").fieldNames().forEachRemaining(roots::add); + } + } + + @Test + void everyYamlRouteExampleOfTheUserManualValidates() throws Exception { + Map<String, String> pages = currentPages(); + assumeTrue(!pages.isEmpty(), "the user manual is only checked inside the Camel source tree"); + + DocExamples result = validate(pages); + + assertThat(roots).as("the roots of the YAML DSL schema").contains("route", "beans", "rest", "routeTemplate"); + assertThat(result.examples()).as("YAML route examples found in the user manual").isGreaterThan(150); + assertThat(result.failures()).as("User manual examples that do not validate").isEmpty(); + } + + @Test + void theCheckSeesWhatItIsFor() throws Exception { + String page + = "[source,yaml]\n----\n- route:\n from:\n uri: direct:a\n steps:\n - to:\n uri: mock:b\n----\n" + + "[source,yaml]\n----\n- route:\n templateParameters:\n - name: x\n----\n" + + "[source,yaml]\n----\n- setBody:\n simple: hello\n----\n" + + "[source,yaml]\n----\n- require: '@antora/lunr'\n----\n"; + + DocExamples result = validate(Map.of("fake", page)); + + assertThat(result.examples()).as("the two route examples, not the step or the Antora entry").isEqualTo(2); + assertThat(result.failures()).hasSize(1); + assertThat(result.failures().get(0)).startsWith("fake.adoc:12 example 2:"); + } + + private static DocExamples validate(Map<String, String> pages) throws Exception { + int examples = 0; + List<String> failures = new ArrayList<>(); + for (Map.Entry<String, String> entry : pages.entrySet()) { + String page = entry.getKey(); + String doc = entry.getValue(); + int n = 0; + Matcher m = YAML_BLOCK.matcher(doc); + while (m.find()) { + String yaml = m.group(1).stripTrailing() + "\n"; + Matcher key = FIRST_KEY.matcher(yaml.stripLeading()); + if (!key.find() || !roots.contains(key.group(1))) { + // a step or an option fragment, or the YAML of another tool: not a route file + continue; + } + n++; + examples++; + List<Error> errors = validator.validate(yaml); + if (!errors.isEmpty()) { + int line = 1 + countLines(doc, m.start(1)); + failures.add(page + ".adoc:" + line + " example " + n + ": " + errors.get(0).getMessage()); + } + } + } + return new DocExamples(examples, failures); + } + + /** + * The pages of the user manual that document the current release, by name, or an empty map outside the source tree. + */ + private static Map<String, String> currentPages() throws Exception { + Map<String, String> answer = new TreeMap<>(); + Path dir = Path.of("").toAbsolutePath(); + while (dir != null && !(Files.isDirectory(dir.resolve(PAGES)) && Files.isDirectory(dir.resolve("components")))) { + dir = dir.getParent(); + } + if (dir == null) { + return answer; + } + try (Stream<Path> files = Files.walk(dir.resolve(PAGES))) { + for (Path file : files.filter(f -> f.toString().endsWith(".adoc")).toList()) { + String name = file.getFileName().toString(); + name = name.substring(0, name.length() - 5); + if (name.contains("upgrade-guide") || name.contains("migration-guide")) { + continue; + } + answer.put(name, Files.readString(file)); + } + } + return answer; + } + + private static int countLines(String text, int end) { + int n = 0; + for (int i = 0; i < end; i++) { + if (text.charAt(i) == '\n') { + n++; + } + } + return n; + } +}
