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&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..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&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 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&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..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;
+    }
+}

Reply via email to