This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new c325bd9cb584 CAMEL-24774: docs - build-time guards for the XML 
examples (against the schemas), the Java imports and the user-manual YAML of 
the documentation, and the examples the first run found (#26525)
c325bd9cb584 is described below

commit c325bd9cb58429ec4e4099378d3050c4747846e3
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 16 21:05:18 2026 +0200

    CAMEL-24774: docs - build-time guards for the XML examples (against the 
schemas), the Java imports and the user-manual YAML of the documentation, and 
the examples the first run found (#26525)
    
    Three more 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), 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 21 examples the first run found 
are corrected (cometd, netty, seda, process-eip, split-eip, scatter-gather, 
resequence-eip, unm [...]
    
    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    | 406 +++++++++++++++++++++
 .../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  | 163 +++++++++
 29 files changed, 962 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&amp;timeout=240000&amp;interval=0&amp;maxInterval=30000&amp;multiFrameInterval=1500&amp;jsonCommented=true&amp;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&amp;ssl=true&amp;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..62e6f60d0b98
--- /dev/null
+++ 
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java
@@ -0,0 +1,406 @@
+/*
+ * 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&paramB=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 fragments are judged as the global elements of the schema; the 
set must be the real one
+        assertTrue(springGlobalElements.size() > 200 && 
springGlobalElements.contains("route")
+                && springGlobalElements.contains("onException") && 
springGlobalElements.contains("marshal"),
+                "global elements of camel-spring.xsd: " + 
springGlobalElements.size());
+        // 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: the {@code xs:element}
+     * children of the {@code xs:schema} root, read from the parsed schema so 
the formatting of the generated file does
+     * not matter.
+     */
+    private static Set<String> globalElements(String xsd) throws Exception {
+        Set<String> answer = new HashSet<>();
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl";, 
true);
+        Document schema = factory.newDocumentBuilder().parse(new 
InputSource(new StringReader(xsd)));
+        NodeList children = schema.getDocumentElement().getChildNodes();
+        for (int i = 0; i < children.getLength(); i++) {
+            Node child = children.item(i);
+            if (child.getNodeType() == Node.ELEMENT_NODE && 
"element".equals(child.getLocalName())
+                    && 
XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(child.getNamespaceURI())) {
+                answer.add(((Element) child).getAttribute("name"));
+            }
+        }
+        if (answer.isEmpty()) {
+            throw new IllegalStateException("No global elements found in the 
schema, the fragments would not be judged");
+        }
+        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&amp;timeout=240000&amp;interval=0&amp;maxInterval=30000&amp;multiFrameInterval=1500&amp;jsonCommented=true&amp;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&amp;ssl=true&amp;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..174a198934f5
--- /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,163 @@
+/*
+ * 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.
+     * The source tree is found as {@code UserManualPages.repositoryRoot()} in 
the camel-catalog tests finds it: the
+     * directory up the tree that holds both the user manual pages and the 
components; keep the two in step.
+     */
+    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;
+    }
+}

Reply via email to