You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by "davsclaus (via GitHub)" <gi...@apache.org> on 2026/09/16 18:19:31 UTC
[PR] 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 [camel]
davsclaus opened a new pull request, #26525:
URL: https://github.com/apache/camel/pull/26525
Resolves [CAMEL-24774](https://issues.apache.org/jira/browse/CAMEL-24774). The census behind CAMEL-24773 (#26512) ran three checks the existing guards (`EipDocExamplesTest`, `CatalogDocExamplesTest`, CAMEL-24693/24710) cannot make, and each found real mistakes on its first run. This PR turns them into build-time tests, in the same shape as the existing ones, and corrects what they found.
## The guards
**`DocExamplesXmlSchemaTest`** (camel-catalog) — every `[source,xml]` block of the catalog pages, and of the user manual when the tests run inside the source tree, validated against the schemas bundled in the catalog:
- a `<camelContext>` whole against `camel-spring.xsd`, as Spring XML validates it (so the element order inside is checked, it is a real constraint there)
- an xml-io root (`<camel>`, `<routes>`, `<rests>`…) against `camel-xml-io.xsd`
- any other Camel element on its own (`<route>`, `<onException>`, `<marshal>`…) as the global element it is in `camel-spring.xsd` — every fragment root is one — so a fragment is judged without a wrapper imposing an order the page never meant
- a block is judged only when it contains an unprefixed Camel element (`<route`, `<from`, `<marshal`, `<xpath`…): XML-Signature payloads, XSLT, StAX records and MyBatis mappers share element names with the schema and are left alone; so are top-level Spring `<bean>`s, `...` placeholder lines, AsciiDoc callouts and undeclared prefixes (`camel:`, `prop:`) of partial snippets
- a skip list by page and marker text, as `EipDocExamplesTest` has, for the four examples the schemas cannot judge: two elided snippets, the endpoint page's unescaped `&` that is there to show the error, and the `<namespace>` child of `<xtokenize>` on two pages — the model and the xml-io parser support it, but no generated schema can express it next to the expression text (JAXB `@XmlValue`), so Spring XML declares namespaces as `xmlns:` attributes instead
**`DocExamplesJavaImportsTest`** (camel-catalog) — every `import org.apache.camel…` line of a Java example, in the catalog pages and the user manual, resolved against `src/main/java` and `src/generated/java` of the source tree (nested classes, static members and wildcards handled). 2 seconds for the whole tree.
**`UserManualDocExamplesTest`** (camel-yaml-dsl-validator) — the YAML route examples of the user manual through the YAML validator, as `EipDocExamplesTest` does for the catalog pages. A block is a route example when its first entry is a root of the YAML DSL schema (`- route:`, `- beans:`, `- rest:`…, read from the schema), so a step written on its own or the YAML of another tool is not judged.
The user-manual checks read the pages from the source tree and are skipped (JUnit assumption) outside it; the upgrade and migration guides are left out, they show the syntax of older releases on purpose. Each guard has a `theCheckSeesWhatItIsFor` test with crafted good and bad examples, so a guard that silently judges nothing fails too.
## What the first run found (fixed here, catalog mirror regenerated)
- **cometd** — `<camel:trustManagers>` closed by `</camel:keyManagers>`, and five unescaped `&` in a `<to uri>`
- **netty** — unescaped `&` in a `<to uri>`; **seda** — a second `<from>seda:…</from>` with text content (#26512 fixed the other one)
- **process-eip** — `<routes>` inside `<camelContext>`; **split-eip** — a Spring `<bean>` inside `<camelContext>`; **scatter-gather** — a block of Spring beans labelled `<camelContext>`
- **resequence-eip** ×2 — `<batchConfig>`/`<streamConfig>` after the expression; the schema has the config element first
- **unmarshal-eip**, **routes** (manual) — `<jaxb/>` without the required `contextPath` (the YAML tab of the routes page has it)
- **xpath-language** — `<xpath headerName="…">` and `xpath(text, "header")`: `headerName` is gone since 4.0, the 4.x form is `source="header:…"` and the fluent builder, as the jsonpath page documents it
- **univocityCsv / univocityFixed / univocityTsv** — all 15 XML examples use the Camel 2 element names `<univocity-csv>`, `<univocity-fixed>`, `<univocity-tsv>`, `<univocity-header>`; the elements are `univocityCsv`, `univocityFixed`, `univocityTsv`, `univocityHeader` since Camel 3
Counts on this branch: about 1,800 catalog and 273 user-manual XML examples validate, 143 imports resolve, and the 207 user-manual YAML route examples pass. camel-catalog (1061 tests) and camel-yaml-dsl-validator (107) suites pass.
_Claude Code on behalf of @davsclaus_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] 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 [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #26525:
URL: https://github.com/apache/camel/pull/26525#issuecomment-5702413191
:star2: Thank you for your contribution to the Apache Camel project! :star2:
:robot: CI automation will test this PR automatically.
:camel: Apache Camel Committers, please review the following items:
* First-time contributors **require MANUAL approval** for the GitHub Actions to run
* You can use the command `/component-test (camel-)component-name1 (camel-)component-name2..` to request a test from the test bot although they are normally detected and executed by CI.
* You can label PRs using `skip-tests` and `test-dependents` to fine-tune the checks executed by this PR.
* Build and test logs are available in the summary page. **Only** [Apache Camel committers](https://camel.apache.org/community/team/#committers) have access to the summary.
:warning: Be careful when sharing logs. Review their contents before sharing them publicly.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] 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 [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #26525:
URL: https://github.com/apache/camel/pull/26525#discussion_r4029545259
##########
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");
Review Comment:
Applied in a168161c20a3, thanks — `-{4,}` as in the XML guard.
_Claude Code on behalf of @davsclaus_
##########
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;
Review Comment:
Kept as two copies (different modules, no shared test-support module between camel-catalog and camel-yaml-dsl-validator, and it is four lines), with a comment on each pointing at the other so a change to the criteria is made in both — a168161c20a3.
_Claude Code on behalf of @davsclaus_
##########
catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java:
##########
@@ -0,0 +1,387 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.catalog;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.Validator;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXParseException;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * The XML examples of the documentation bundled in the catalog must validate against the XML schemas bundled next to
+ * them (CAMEL-24774): a {@code <camelContext>} against camel-spring.xsd as Spring XML validates it, the xml-io roots
+ * ({@code <camel>}, {@code <routes>}...) against camel-xml-io.xsd, and any other Camel element written on its own
+ * ({@code <route>}, {@code <onException>}, {@code <marshal>}...) as the global element it is in camel-spring.xsd, so a
+ * fragment is checked without a wrapper imposing an element order the page never meant.
+ * <p/>
+ * The examples are what people copy: a Camel 1.x namespace, an attribute renamed in Camel 3, a tag closed by another
+ * tag or a {@code &} in a URI fails the build here instead of in the reader's application.
+ */
+class DocExamplesXmlSchemaTest {
+
+ private static final Pattern XML_BLOCK = Pattern.compile("\\[source,xml\\]\\n-{4,}\\n(.*?)\\n-{4,}", Pattern.DOTALL);
+ private static final Pattern CALLOUT = Pattern.compile("\\s*<\\d+>\\s*$", Pattern.MULTILINE);
+ private static final Pattern XML_DECLARATION = Pattern.compile("<\\?xml[^>]*\\?>");
+ private static final Pattern CAMEL_CONTEXT
+ = Pattern.compile("<(\\w+:)?camelContext\\b.*?</(\\w+:)?camelContext>", Pattern.DOTALL);
+ /**
+ * An element that makes a block a Camel example rather than a payload, a Maven POM, an XSLT or a configuration file
+ * of another project which happens to share element names with the Camel schema.
+ */
+ private static final Pattern CAMEL_SIGNATURE = Pattern.compile("<(camelContext|routes|route|from|to|toD|"
+ + "marshal|unmarshal|dataFormats|rest|restConfiguration|"
+ + "routeTemplate|templatedRoute|routeConfiguration|"
+ + "onException|errorHandler|intercept|interceptFrom|"
+ + "interceptSendToEndpoint|onCompletion|setHeader|setBody|"
+ + "setProperty|setVariable|choice|filter|split|aggregate|"
+ + "multicast|recipientList|wireTap|log|process|transform|"
+ + "convertBodyTo|validate|enrich|pollEnrich|threads|threadPool|"
+ + "propertyPlaceholder|globalOptions|streamCaching|jmxAgent|"
+ + "sslContextParameters|xpath|simple|csimple|jsonpath|jq|"
+ + "xtokenize|tokenize|xquery|groovy|datasonnet|mvel|ognl)"
+ + "[\\s/>]");
+
+ private static final String SPRING_NS = "http://camel.apache.org/schema/spring";
+ private static final String XML_IO_NS = "http://camel.apache.org/schema/xml-io";
+ private static final String SPRING_BEANS_NS = "http://www.springframework.org/schema/beans";
+
+ /** The roots of an xml-io file, validated against camel-xml-io.xsd. */
+ private static final Set<String> XML_IO_ROOTS
+ = Set.of("camel", "routes", "rests", "routeTemplates", "templatedRoutes", "routeConfigurations");
+
+ /**
+ * Top-level elements that are not judged on their own: a Spring {@code <bean>} and its {@code <property>} share
+ * their names with Camel elements, and the rest are Maven, XSLT or payload samples.
+ */
+ private static final Set<String> IGNORED_ROOTS = Set.of("bean", "beans", "property", "dependency", "dependencies",
+ "dependencyManagement", "plugin", "plugins", "build", "project", "extensions");
+
+ /**
+ * Examples the schemas cannot judge, by page and a text found in the example: an elided {@code <beans ...>} start
+ * tag, a Spring bean next to route fragments, and the {@code <namespace>} child of an expression, which the model
+ * and the xml-io parser support but no generated schema can express next to the expression text (JAXB
+ * {@code @XmlValue}), so Spring XML declares the namespaces as {@code xmlns:} attributes instead; and the endpoint
+ * page's unescaped {@code &}, which is there to show the error it causes.
+ */
+ private static final Map<String, String> EXAMPLES_SKIPPED = Map.of(
+ "xmlsecurity-sign-component", "<bean id=\"xadesProperties\"",
+ "xmlsecurity-verify-component", "<bean id=\"xadesProperties\"",
+ "spring-summary", "<beans xmlns=\"http://www.springframework.org/schema/beans\"",
+ "split-eip", "<namespace key=",
+ "xtokenize-language", "<namespace key=",
+ "endpoint", "paramA=1¶mB=2");
+
+ private static CamelCatalog catalog;
+ private static Schema springSchema;
+ private static Schema xmlIoSchema;
+ private static Set<String> springGlobalElements;
+
+ private record DocExamples(int examples, List<String> failures) {
+ }
+
+ @BeforeAll
+ static void setup() throws Exception {
+ catalog = new DefaultCamelCatalog();
+ SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ springSchema = factory.newSchema(new StreamSource(new StringReader(catalog.springSchemaAsXml())));
+ xmlIoSchema = factory.newSchema(new StreamSource(new StringReader(catalog.xmlIoSchemaAsXml())));
+ springGlobalElements = globalElements(catalog.springSchemaAsXml());
+ }
+
+ @Test
+ void everyXmlExampleOfTheDocumentationValidates() throws Exception {
+ List<String> pages = new ArrayList<>(catalog.findDocNames());
+
+ DocExamples result = validate(pages);
+
+ assertTrue(result.examples() > 1500, "XML examples found in the documentation: " + result.examples());
+ assertTrue(result.failures().isEmpty(),
+ "Documentation XML examples that do not validate:\n " + String.join("\n ", result.failures()));
+ }
+
+ @Test
+ void everyXmlExampleOfTheUserManualValidates() throws Exception {
+ Map<String, String> pages = UserManualPages.currentPages();
+ assumeTrue(!pages.isEmpty(), "the user manual is only checked inside the Camel source tree");
+
+ DocExamples result = validate(pages);
+
+ assertTrue(result.examples() > 200, "XML examples found in the user manual: " + result.examples());
+ assertTrue(result.failures().isEmpty(),
+ "User manual XML examples that do not validate:\n " + String.join("\n ", result.failures()));
+ }
+
+ @Test
+ void theCheckSeesWhatItIsFor() {
+ // the classes of mistakes the check exists for
+ assertTrue(validateBlock("<camelContext xmlns=\"http://activemq.apache.org/camel/schema/spring\">\n"
+ + "<route><from uri=\"direct:a\"/><to uri=\"mock:b\"/></route></camelContext>")
+ .toString().contains("camelContext"), "the Camel 1.x namespace");
+ assertTrue(!validateBlock("<from>seda:a?size=1</from>").isEmpty(), "a from with text content");
+ assertTrue(!validateBlock("<onCompletion executorServiceRef=\"x\"><to uri=\"mock:a\"/></onCompletion>").isEmpty(),
+ "a Camel 2 attribute");
+ assertTrue(!validateBlock("<route><from uri=\"direct:a\"/><to uri=\"mock:b?x=1&y=2\"/></route>").isEmpty(),
+ "an unescaped ampersand");
+ assertTrue(!validateBlock("<route><from uri=\"direct:a\"/><marshal><jaxb/></marshal></route>").isEmpty(),
+ "a missing required attribute");
+ assertTrue(!validateBlock("<onException><redeliveryPolicy maximumRedeliveries=\"1\"/>"
+ + "<exception>java.io.IOException</exception></onException>")
+ .isEmpty(),
+ "the element order Spring XML enforces");
+ // and what it leaves alone
+ assertTrue(validateBlock("<route id=\"a\"><from uri=\"direct:a\"/><to uri=\"mock:b\"/></route>\n"
+ + "<onException><exception>java.io.IOException</exception><to uri=\"mock:e\"/></onException>")
+ .isEmpty(), "valid fragments in any order");
+ assertTrue(validateBlock("<routes><route><from uri=\"direct:a\"/><to uri=\"mock:b\"/></route></routes>").isEmpty(),
+ "an xml-io root");
+ assertTrue(validateBlock("<mapper><delete id=\"x\" parameterType=\"int\">delete from t</delete></mapper>").isEmpty(),
+ "a MyBatis mapper sharing element names");
+ assertTrue(validateBlock("<bean id=\"a\" class=\"com.foo.A\"/>\n<from uri=\"direct:a\"/> <1>\n...").isEmpty(),
+ "a Spring bean, a callout and a placeholder line");
+ }
+
+ @Test
+ void theSkippedPagesStillExist() throws Exception {
+ Map<String, String> manual = UserManualPages.pages();
+ for (String page : EXAMPLES_SKIPPED.keySet()) {
+ assertTrue(catalog.asciiDoc(page) != null || manual.containsKey(page),
+ "the skipped page " + page + " is gone, drop the entry");
+ }
+ }
+
+ private static DocExamples validate(List<String> pages) throws Exception {
+ Map<String, String> docs = new LinkedHashMap<>();
+ for (String page : pages) {
+ String doc = catalog.asciiDoc(page);
+ if (doc != null) {
+ docs.put(page, doc);
+ }
+ }
+ return validate(docs);
+ }
+
+ private static DocExamples validate(Map<String, String> pages) {
+ int examples = 0;
+ List<String> failures = new ArrayList<>();
+ for (Map.Entry<String, String> entry : pages.entrySet()) {
+ String page = entry.getKey();
+ String doc = entry.getValue();
+ int n = 0;
+ Matcher m = XML_BLOCK.matcher(doc);
+ while (m.find()) {
+ n++;
+ String xml = m.group(1);
+ String skipped = EXAMPLES_SKIPPED.get(page);
+ if (skipped != null && xml.contains(skipped)) {
+ continue;
+ }
+ int line = 1 + countLines(doc, m.start(1));
+ for (String error : validateBlock(xml)) {
+ failures.add(page + ".adoc:" + line + " example " + n + ": " + error);
+ }
+ examples++;
+ }
+ }
+ return new DocExamples(examples, failures);
+ }
+
+ /**
+ * Validates one XML example and returns what is wrong with it (empty when it validates or is not an example the
+ * schemas can judge).
+ */
+ static List<String> validateBlock(String xml) {
+ List<String> errors = new ArrayList<>();
+ String text = clean(xml);
+ if (text.isBlank() || !CAMEL_SIGNATURE.matcher(text).find()) {
+ // a payload, a POM, an XSLT: not something the Camel schemas describe
+ return errors;
+ }
+ if (text.contains("<camelContext") || text.contains(":camelContext")) {
+ Matcher cm = CAMEL_CONTEXT.matcher(text);
+ while (cm.find()) {
+ String context = cm.group(0);
+ if (cm.group(1) != null) {
+ String prefix = cm.group(1).substring(0, cm.group(1).length() - 1);
+ if (!context.contains("xmlns:" + prefix + "=")) {
+ context = context.replaceFirst("<" + prefix + ":camelContext",
+ "<" + prefix + ":camelContext xmlns:" + prefix + "=\"" + SPRING_NS + "\"");
+ }
+ } else {
+ context = withDefaultNamespace(context, "camelContext", SPRING_NS);
+ }
+ validate(context, springSchema, errors);
+ }
+ return errors;
+ }
+
+ List<Element> roots;
+ try {
+ roots = topLevelElements(text);
+ } catch (SAXParseException e) {
+ errors.add("not well-formed XML: " + e.getMessage());
+ return errors;
+ } catch (Exception e) {
+ errors.add("not well-formed XML: " + e);
+ return errors;
+ }
+ for (Element root : roots) {
+ String name = root.getNodeName();
+ String local = name.contains(":") ? name.substring(name.indexOf(':') + 1) : name;
+ String ns = root.getAttribute("xmlns");
+ if (SPRING_BEANS_NS.equals(ns) || IGNORED_ROOTS.contains(local) || name.contains(":")) {
+ // a Spring beans file, a Maven or payload sample, or a prefixed element of another schema
+ continue;
+ }
+ String element = serialize(root);
+ if (element == null) {
+ // an undeclared prefix (prop:, camel:...) the page declares elsewhere, nothing to judge
+ continue;
+ }
+ if (XML_IO_ROOTS.contains(local)) {
+ validate(withDefaultNamespace(element, local, XML_IO_NS), xmlIoSchema, errors);
+ } else if (springGlobalElements.contains(local)) {
+ validate(withDefaultNamespace(element, local, SPRING_NS), springSchema, errors);
+ }
+ }
+ return errors;
+ }
+
+ private static void validate(String document, Schema schema, List<String> errors) {
+ try {
+ Validator validator = schema.newValidator();
+ validator.validate(new StreamSource(new StringReader(document)));
+ } catch (SAXParseException e) {
+ String msg = e.getMessage();
+ if (msg != null && msg.contains("is not bound")) {
+ // a fragment using a prefix its page declares elsewhere (camel:, prop:, xsl:...), nothing to judge
+ return;
+ }
+ errors.add(msg);
+ } catch (Exception e) {
+ errors.add(e.toString());
+ }
+ }
+
+ /** Drops the XML declaration, the placeholder lines and the AsciiDoc callouts, which are not part of the XML. */
+ static String clean(String xml) {
+ String text = XML_DECLARATION.matcher(xml).replaceAll("");
+ text = CALLOUT.matcher(text).replaceAll("");
+ StringBuilder sb = new StringBuilder();
+ for (String line : text.split("\n")) {
+ String t = line.trim();
+ if (t.equals("...") || t.equals("…")) {
+ continue;
+ }
+ sb.append(line).append('\n');
+ }
+ return sb.toString();
+ }
+
+ /** Adds the default namespace to the root element when the example does not declare one. */
+ private static String withDefaultNamespace(String document, String root, String ns) {
+ int end = document.indexOf('>');
+ String startTag = end > 0 ? document.substring(0, end) : document;
+ if (startTag.contains("xmlns=")) {
+ return document;
+ }
+ return document.replaceFirst("<" + root + "\\b", "<" + root + " xmlns=\"" + ns + "\"");
+ }
+
+ /** The top-level elements of a fragment, parsed without namespaces so undeclared prefixes do not matter. */
+ private static List<Element> topLevelElements(String fragment) throws Exception {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(false);
+ factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document dom = builder.parse(new InputSource(new StringReader("<wrap>" + fragment + "</wrap>")));
+ List<Element> answer = new ArrayList<>();
+ NodeList children = dom.getDocumentElement().getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ answer.add((Element) child);
+ }
+ }
+ return answer;
+ }
+
+ private static String serialize(Element element) {
+ try {
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ StringWriter sw = new StringWriter();
+ transformer.transform(new DOMSource(element), new StreamResult(sw));
+ return sw.toString();
+ } catch (Exception e) {
+ // an attribute with a prefix the fragment does not declare cannot be serialized
+ return null;
+ }
+ }
+
+ /** The names of the global elements of a schema: the elements an example can start with. */
+ private static Set<String> globalElements(String xsd) {
+ Set<String> answer = new HashSet<>();
+ Matcher m = Pattern.compile("\n <xs:element name=\"([A-Za-z-]+)\"").matcher(xsd);
Review Comment:
Agreed, that was silently-skip-everything territory. In a168161c20a3 the global elements are collected from the parsed XSD (`xs:element` children of `xs:schema`), `globalElements()` throws when the set comes back empty, and `theCheckSeesWhatItIsFor` asserts the set is the real one (200+ names including `route`, `onException`, `marshal`).
_Claude Code on behalf of @davsclaus_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] 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 [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #26525:
URL: https://github.com/apache/camel/pull/26525#discussion_r4029367361
##########
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");
Review Comment:
⚠️ **Pattern inconsistency — will miss YAML blocks delimited with 5+ dashes**
`XML_BLOCK` in `DocExamplesXmlSchemaTest` uses `-{4,}` (four *or more* dashes), correctly matching any AsciiDoc code block delimiter. `YAML_BLOCK` here uses `-{4}` (exactly four). AsciiDoc allows any length ≥ 4, and some pages use longer delimiters (e.g. `-------`). A YAML route example inside a 5-dash block is silently skipped without error.
```suggestion
private static final Pattern YAML_BLOCK = Pattern.compile("\\[source,yaml\\]\\n-{4,}\\n(.*?)\\n-{4,}", Pattern.DOTALL);
```
##########
catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java:
##########
@@ -0,0 +1,387 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.catalog;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.Validator;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXParseException;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * The XML examples of the documentation bundled in the catalog must validate against the XML schemas bundled next to
+ * them (CAMEL-24774): a {@code <camelContext>} against camel-spring.xsd as Spring XML validates it, the xml-io roots
+ * ({@code <camel>}, {@code <routes>}...) against camel-xml-io.xsd, and any other Camel element written on its own
+ * ({@code <route>}, {@code <onException>}, {@code <marshal>}...) as the global element it is in camel-spring.xsd, so a
+ * fragment is checked without a wrapper imposing an element order the page never meant.
+ * <p/>
+ * The examples are what people copy: a Camel 1.x namespace, an attribute renamed in Camel 3, a tag closed by another
+ * tag or a {@code &} in a URI fails the build here instead of in the reader's application.
+ */
+class DocExamplesXmlSchemaTest {
+
+ private static final Pattern XML_BLOCK = Pattern.compile("\\[source,xml\\]\\n-{4,}\\n(.*?)\\n-{4,}", Pattern.DOTALL);
+ private static final Pattern CALLOUT = Pattern.compile("\\s*<\\d+>\\s*$", Pattern.MULTILINE);
+ private static final Pattern XML_DECLARATION = Pattern.compile("<\\?xml[^>]*\\?>");
+ private static final Pattern CAMEL_CONTEXT
+ = Pattern.compile("<(\\w+:)?camelContext\\b.*?</(\\w+:)?camelContext>", Pattern.DOTALL);
+ /**
+ * An element that makes a block a Camel example rather than a payload, a Maven POM, an XSLT or a configuration file
+ * of another project which happens to share element names with the Camel schema.
+ */
+ private static final Pattern CAMEL_SIGNATURE = Pattern.compile("<(camelContext|routes|route|from|to|toD|"
+ + "marshal|unmarshal|dataFormats|rest|restConfiguration|"
+ + "routeTemplate|templatedRoute|routeConfiguration|"
+ + "onException|errorHandler|intercept|interceptFrom|"
+ + "interceptSendToEndpoint|onCompletion|setHeader|setBody|"
+ + "setProperty|setVariable|choice|filter|split|aggregate|"
+ + "multicast|recipientList|wireTap|log|process|transform|"
+ + "convertBodyTo|validate|enrich|pollEnrich|threads|threadPool|"
+ + "propertyPlaceholder|globalOptions|streamCaching|jmxAgent|"
+ + "sslContextParameters|xpath|simple|csimple|jsonpath|jq|"
+ + "xtokenize|tokenize|xquery|groovy|datasonnet|mvel|ognl)"
+ + "[\\s/>]");
+
+ private static final String SPRING_NS = "http://camel.apache.org/schema/spring";
+ private static final String XML_IO_NS = "http://camel.apache.org/schema/xml-io";
+ private static final String SPRING_BEANS_NS = "http://www.springframework.org/schema/beans";
+
+ /** The roots of an xml-io file, validated against camel-xml-io.xsd. */
+ private static final Set<String> XML_IO_ROOTS
+ = Set.of("camel", "routes", "rests", "routeTemplates", "templatedRoutes", "routeConfigurations");
+
+ /**
+ * Top-level elements that are not judged on their own: a Spring {@code <bean>} and its {@code <property>} share
+ * their names with Camel elements, and the rest are Maven, XSLT or payload samples.
+ */
+ private static final Set<String> IGNORED_ROOTS = Set.of("bean", "beans", "property", "dependency", "dependencies",
+ "dependencyManagement", "plugin", "plugins", "build", "project", "extensions");
+
+ /**
+ * Examples the schemas cannot judge, by page and a text found in the example: an elided {@code <beans ...>} start
+ * tag, a Spring bean next to route fragments, and the {@code <namespace>} child of an expression, which the model
+ * and the xml-io parser support but no generated schema can express next to the expression text (JAXB
+ * {@code @XmlValue}), so Spring XML declares the namespaces as {@code xmlns:} attributes instead; and the endpoint
+ * page's unescaped {@code &}, which is there to show the error it causes.
+ */
+ private static final Map<String, String> EXAMPLES_SKIPPED = Map.of(
+ "xmlsecurity-sign-component", "<bean id=\"xadesProperties\"",
+ "xmlsecurity-verify-component", "<bean id=\"xadesProperties\"",
+ "spring-summary", "<beans xmlns=\"http://www.springframework.org/schema/beans\"",
+ "split-eip", "<namespace key=",
+ "xtokenize-language", "<namespace key=",
+ "endpoint", "paramA=1¶mB=2");
+
+ private static CamelCatalog catalog;
+ private static Schema springSchema;
+ private static Schema xmlIoSchema;
+ private static Set<String> springGlobalElements;
+
+ private record DocExamples(int examples, List<String> failures) {
+ }
+
+ @BeforeAll
+ static void setup() throws Exception {
+ catalog = new DefaultCamelCatalog();
+ SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ springSchema = factory.newSchema(new StreamSource(new StringReader(catalog.springSchemaAsXml())));
+ xmlIoSchema = factory.newSchema(new StreamSource(new StringReader(catalog.xmlIoSchemaAsXml())));
+ springGlobalElements = globalElements(catalog.springSchemaAsXml());
+ }
+
+ @Test
+ void everyXmlExampleOfTheDocumentationValidates() throws Exception {
+ List<String> pages = new ArrayList<>(catalog.findDocNames());
+
+ DocExamples result = validate(pages);
+
+ assertTrue(result.examples() > 1500, "XML examples found in the documentation: " + result.examples());
+ assertTrue(result.failures().isEmpty(),
+ "Documentation XML examples that do not validate:\n " + String.join("\n ", result.failures()));
+ }
+
+ @Test
+ void everyXmlExampleOfTheUserManualValidates() throws Exception {
+ Map<String, String> pages = UserManualPages.currentPages();
+ assumeTrue(!pages.isEmpty(), "the user manual is only checked inside the Camel source tree");
+
+ DocExamples result = validate(pages);
+
+ assertTrue(result.examples() > 200, "XML examples found in the user manual: " + result.examples());
+ assertTrue(result.failures().isEmpty(),
+ "User manual XML examples that do not validate:\n " + String.join("\n ", result.failures()));
+ }
+
+ @Test
+ void theCheckSeesWhatItIsFor() {
+ // the classes of mistakes the check exists for
+ assertTrue(validateBlock("<camelContext xmlns=\"http://activemq.apache.org/camel/schema/spring\">\n"
+ + "<route><from uri=\"direct:a\"/><to uri=\"mock:b\"/></route></camelContext>")
+ .toString().contains("camelContext"), "the Camel 1.x namespace");
+ assertTrue(!validateBlock("<from>seda:a?size=1</from>").isEmpty(), "a from with text content");
+ assertTrue(!validateBlock("<onCompletion executorServiceRef=\"x\"><to uri=\"mock:a\"/></onCompletion>").isEmpty(),
+ "a Camel 2 attribute");
+ assertTrue(!validateBlock("<route><from uri=\"direct:a\"/><to uri=\"mock:b?x=1&y=2\"/></route>").isEmpty(),
+ "an unescaped ampersand");
+ assertTrue(!validateBlock("<route><from uri=\"direct:a\"/><marshal><jaxb/></marshal></route>").isEmpty(),
+ "a missing required attribute");
+ assertTrue(!validateBlock("<onException><redeliveryPolicy maximumRedeliveries=\"1\"/>"
+ + "<exception>java.io.IOException</exception></onException>")
+ .isEmpty(),
+ "the element order Spring XML enforces");
+ // and what it leaves alone
+ assertTrue(validateBlock("<route id=\"a\"><from uri=\"direct:a\"/><to uri=\"mock:b\"/></route>\n"
+ + "<onException><exception>java.io.IOException</exception><to uri=\"mock:e\"/></onException>")
+ .isEmpty(), "valid fragments in any order");
+ assertTrue(validateBlock("<routes><route><from uri=\"direct:a\"/><to uri=\"mock:b\"/></route></routes>").isEmpty(),
+ "an xml-io root");
+ assertTrue(validateBlock("<mapper><delete id=\"x\" parameterType=\"int\">delete from t</delete></mapper>").isEmpty(),
+ "a MyBatis mapper sharing element names");
+ assertTrue(validateBlock("<bean id=\"a\" class=\"com.foo.A\"/>\n<from uri=\"direct:a\"/> <1>\n...").isEmpty(),
+ "a Spring bean, a callout and a placeholder line");
+ }
+
+ @Test
+ void theSkippedPagesStillExist() throws Exception {
+ Map<String, String> manual = UserManualPages.pages();
+ for (String page : EXAMPLES_SKIPPED.keySet()) {
+ assertTrue(catalog.asciiDoc(page) != null || manual.containsKey(page),
+ "the skipped page " + page + " is gone, drop the entry");
+ }
+ }
+
+ private static DocExamples validate(List<String> pages) throws Exception {
+ Map<String, String> docs = new LinkedHashMap<>();
+ for (String page : pages) {
+ String doc = catalog.asciiDoc(page);
+ if (doc != null) {
+ docs.put(page, doc);
+ }
+ }
+ return validate(docs);
+ }
+
+ private static DocExamples validate(Map<String, String> pages) {
+ int examples = 0;
+ List<String> failures = new ArrayList<>();
+ for (Map.Entry<String, String> entry : pages.entrySet()) {
+ String page = entry.getKey();
+ String doc = entry.getValue();
+ int n = 0;
+ Matcher m = XML_BLOCK.matcher(doc);
+ while (m.find()) {
+ n++;
+ String xml = m.group(1);
+ String skipped = EXAMPLES_SKIPPED.get(page);
+ if (skipped != null && xml.contains(skipped)) {
+ continue;
+ }
+ int line = 1 + countLines(doc, m.start(1));
+ for (String error : validateBlock(xml)) {
+ failures.add(page + ".adoc:" + line + " example " + n + ": " + error);
+ }
+ examples++;
+ }
+ }
+ return new DocExamples(examples, failures);
+ }
+
+ /**
+ * Validates one XML example and returns what is wrong with it (empty when it validates or is not an example the
+ * schemas can judge).
+ */
+ static List<String> validateBlock(String xml) {
+ List<String> errors = new ArrayList<>();
+ String text = clean(xml);
+ if (text.isBlank() || !CAMEL_SIGNATURE.matcher(text).find()) {
+ // a payload, a POM, an XSLT: not something the Camel schemas describe
+ return errors;
+ }
+ if (text.contains("<camelContext") || text.contains(":camelContext")) {
+ Matcher cm = CAMEL_CONTEXT.matcher(text);
+ while (cm.find()) {
+ String context = cm.group(0);
+ if (cm.group(1) != null) {
+ String prefix = cm.group(1).substring(0, cm.group(1).length() - 1);
+ if (!context.contains("xmlns:" + prefix + "=")) {
+ context = context.replaceFirst("<" + prefix + ":camelContext",
+ "<" + prefix + ":camelContext xmlns:" + prefix + "=\"" + SPRING_NS + "\"");
+ }
+ } else {
+ context = withDefaultNamespace(context, "camelContext", SPRING_NS);
+ }
+ validate(context, springSchema, errors);
+ }
+ return errors;
+ }
+
+ List<Element> roots;
+ try {
+ roots = topLevelElements(text);
+ } catch (SAXParseException e) {
+ errors.add("not well-formed XML: " + e.getMessage());
+ return errors;
+ } catch (Exception e) {
+ errors.add("not well-formed XML: " + e);
+ return errors;
+ }
+ for (Element root : roots) {
+ String name = root.getNodeName();
+ String local = name.contains(":") ? name.substring(name.indexOf(':') + 1) : name;
+ String ns = root.getAttribute("xmlns");
+ if (SPRING_BEANS_NS.equals(ns) || IGNORED_ROOTS.contains(local) || name.contains(":")) {
+ // a Spring beans file, a Maven or payload sample, or a prefixed element of another schema
+ continue;
+ }
+ String element = serialize(root);
+ if (element == null) {
+ // an undeclared prefix (prop:, camel:...) the page declares elsewhere, nothing to judge
+ continue;
+ }
+ if (XML_IO_ROOTS.contains(local)) {
+ validate(withDefaultNamespace(element, local, XML_IO_NS), xmlIoSchema, errors);
+ } else if (springGlobalElements.contains(local)) {
+ validate(withDefaultNamespace(element, local, SPRING_NS), springSchema, errors);
+ }
+ }
+ return errors;
+ }
+
+ private static void validate(String document, Schema schema, List<String> errors) {
+ try {
+ Validator validator = schema.newValidator();
+ validator.validate(new StreamSource(new StringReader(document)));
+ } catch (SAXParseException e) {
+ String msg = e.getMessage();
+ if (msg != null && msg.contains("is not bound")) {
+ // a fragment using a prefix its page declares elsewhere (camel:, prop:, xsl:...), nothing to judge
+ return;
+ }
+ errors.add(msg);
+ } catch (Exception e) {
+ errors.add(e.toString());
+ }
+ }
+
+ /** Drops the XML declaration, the placeholder lines and the AsciiDoc callouts, which are not part of the XML. */
+ static String clean(String xml) {
+ String text = XML_DECLARATION.matcher(xml).replaceAll("");
+ text = CALLOUT.matcher(text).replaceAll("");
+ StringBuilder sb = new StringBuilder();
+ for (String line : text.split("\n")) {
+ String t = line.trim();
+ if (t.equals("...") || t.equals("…")) {
+ continue;
+ }
+ sb.append(line).append('\n');
+ }
+ return sb.toString();
+ }
+
+ /** Adds the default namespace to the root element when the example does not declare one. */
+ private static String withDefaultNamespace(String document, String root, String ns) {
+ int end = document.indexOf('>');
+ String startTag = end > 0 ? document.substring(0, end) : document;
+ if (startTag.contains("xmlns=")) {
+ return document;
+ }
+ return document.replaceFirst("<" + root + "\\b", "<" + root + " xmlns=\"" + ns + "\"");
+ }
+
+ /** The top-level elements of a fragment, parsed without namespaces so undeclared prefixes do not matter. */
+ private static List<Element> topLevelElements(String fragment) throws Exception {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(false);
+ factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document dom = builder.parse(new InputSource(new StringReader("<wrap>" + fragment + "</wrap>")));
+ List<Element> answer = new ArrayList<>();
+ NodeList children = dom.getDocumentElement().getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ answer.add((Element) child);
+ }
+ }
+ return answer;
+ }
+
+ private static String serialize(Element element) {
+ try {
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ StringWriter sw = new StringWriter();
+ transformer.transform(new DOMSource(element), new StreamResult(sw));
+ return sw.toString();
+ } catch (Exception e) {
+ // an attribute with a prefix the fragment does not declare cannot be serialized
+ return null;
+ }
+ }
+
+ /** The names of the global elements of a schema: the elements an example can start with. */
+ private static Set<String> globalElements(String xsd) {
+ Set<String> answer = new HashSet<>();
+ Matcher m = Pattern.compile("\n <xs:element name=\"([A-Za-z-]+)\"").matcher(xsd);
Review Comment:
💡 **Fragile regex for schema global elements — silent empty set if XSD is reformatted**
```java
Matcher m = Pattern.compile("\n <xs:element name=\"([A-Za-z-]+)\"").matcher(xsd);
```
This hardcodes 2-space indentation and assumes `name` is the first attribute. If the schema generator ever changes indentation or attribute order, `springGlobalElements` silently becomes an empty set, and all per-fragment validation is silently skipped (every element falls through both `XML_IO_ROOTS.contains()` and `springGlobalElements.contains()` checks without error — `validateBlock()` returns empty, no failure is recorded).
The `everyXmlExampleOfTheDocumentationValidates` test's `> 1500` assertion is NOT a sufficient guard: it checks that blocks are *found* by `XML_BLOCK`, not that they are validated by the schema — skipped blocks still increment `examples`.
Consider parsing the XSD with a DOM/SAX reader to collect global element names, or at least adding an assertion in `@BeforeAll` that `springGlobalElements` is non-empty:
```java
static void setup() throws Exception {
...
springGlobalElements = globalElements(catalog.springSchemaAsXml());
// guard: if this is empty, fragment validation is silently skipped
if (springGlobalElements.isEmpty()) {
throw new IllegalStateException("No global elements found in spring schema — regex may be broken");
}
}
```
##########
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;
Review Comment:
💡 **Duplicated repository-root-finder — divergence risk**
`UserManualPages.repositoryRoot()` in `camel-catalog` implements the same walk-to-root logic. This class is in a different module so it cannot reuse it directly, but the two implementations will diverge if the detection criteria ever changes (e.g. adding a third directory check). Consider extracting the root-finder into a shared test-support module, or at minimum add a comment cross-referencing `UserManualPages.repositoryRoot()` so they stay in sync.
Also note a minor divergence already: `UserManualPages.repositoryRoot()` uses a `while (dir != null)` loop with a separate `if (condition) return dir` inside — the logic is equivalent but expressed differently. If either copy is changed, the other might not be.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] 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 [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus merged PR #26525:
URL: https://github.com/apache/camel/pull/26525
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] 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 [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #26525:
URL: https://github.com/apache/camel/pull/26525#issuecomment-5702692646
<!-- ci-tested-modules -->
:test_tube: **CI tested the following changed modules:**
- `catalog/camel-catalog`
- `components/camel-cometd`
- `components/camel-netty`
- `components/camel-seda`
- `components/camel-univocity-parsers`
- `components/camel-xpath`
- `core/camel-core-engine`
- `docs`
- `dsl/camel-yaml-dsl/camel-yaml-dsl-validator`
> :information_source: Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the `test-dependents` label to force testing all dependents.
---
<details><summary>:microscope: Scalpel shadow comparison — Scalpel: 497 tested, 25 compile-only — current: 496 all tested</summary>
[Maveniverse Scalpel](https://github.com/maveniverse/scalpel) detected **522 affected modules** (current approach: 496).
<details><summary>:warning: Modules only in Scalpel (27)</summary>
- `apache-camel`
- `camel-allcomponents`
- `camel-catalog`
- `camel-catalog-console`
- `camel-catalog-maven`
- `camel-catalog-suggest`
- `camel-componentdsl`
- `camel-endpointdsl`
- `camel-endpointdsl-support`
- `camel-itest`
- `camel-jbang-core`
- `camel-jbang-it`
- `camel-jbang-main`
- `camel-jbang-plugin-edit`
- `camel-jbang-plugin-generate`
- `camel-jbang-plugin-kubernetes`
- `camel-jbang-plugin-test`
- `camel-kamelet-main`
- `camel-launcher`
- `camel-report-maven-plugin`
- `camel-route-parser`
- `camel-yaml-dsl`
- `camel-yaml-dsl-deserializers`
- `camel-yaml-dsl-maven-plugin`
- `coverage`
- `docs`
- `dummy-component`
</details>
<details><summary>Modules only in current approach (1)</summary>
- `camel-yaml-dsl-validator-maven-plugin`
</details>
**Skip-tests mode would test 497 modules** (9 direct + 488 downstream), **skip tests for 25** (generated code, meta-modules)
<details><summary>Modules Scalpel would test (497)</summary>
- `archetypes`
- `camel-a2a`
- `camel-activemq`
- `camel-activemq6`
- `camel-ai-observability`
- `camel-ai-observability-api`
- `camel-ai-resource`
- `camel-ai-tool`
- `camel-alibaba-eventbridge`
- `camel-alibaba-fc`
- `camel-alibaba-kms`
- `camel-alibaba-mns`
- `camel-alibaba-oss`
- `camel-alibaba-ots`
- `camel-alibaba-sls`
- `camel-alibaba-sms`
- `camel-amqp`
- `camel-api-component-maven-plugin`
- `camel-arangodb`
- `camel-archetype-api-component`
- `camel-archetype-component`
- `camel-archetype-dataformat`
- `camel-archetype-java`
- `camel-archetype-main`
- `camel-as2`
- `camel-as2-api`
- `camel-asn1`
- `camel-asterisk`
- `camel-atmosphere-websocket`
- `camel-atom`
- `camel-attachments`
- `camel-avro`
- `camel-avro-rpc`
- `camel-aws-bedrock`
- `camel-aws-cloudtrail`
- `camel-aws-config`
- `camel-aws-parameter-store`
- `camel-aws-secrets-manager`
- `camel-aws-security-hub`
- `camel-aws2-athena`
- `camel-aws2-comprehend`
- `camel-aws2-cw`
- `camel-aws2-ddb`
- `camel-aws2-ec2`
- `camel-aws2-ecs`
- `camel-aws2-eks`
- `camel-aws2-eventbridge`
- `camel-aws2-iam`
- `camel-aws2-kinesis`
- `camel-aws2-kms`
- `camel-aws2-lambda`
- `camel-aws2-mq`
- `camel-aws2-msk`
- `camel-aws2-polly`
- `camel-aws2-redshift`
- `camel-aws2-rekognition`
- `camel-aws2-s3`
- `camel-aws2-s3-vectors`
- `camel-aws2-ses`
- `camel-aws2-sns`
- `camel-aws2-sqs`
- `camel-aws2-step-functions`
- `camel-aws2-sts`
- `camel-aws2-textract`
- `camel-aws2-timestream`
- `camel-aws2-transcribe`
- `camel-aws2-translate`
- `camel-azure-cosmosdb`
- `camel-azure-eventgrid`
- `camel-azure-eventhubs`
- `camel-azure-files`
- `camel-azure-functions`
- `camel-azure-key-vault`
- `camel-azure-schema-registry`
- `camel-azure-servicebus`
- `camel-azure-storage-blob`
- `camel-azure-storage-datalake`
- `camel-azure-storage-queue`
- `camel-barcode`
- `camel-base64`
- `camel-bean-validator`
- `camel-beanio`
- `camel-bindy`
- `camel-bonita`
- `camel-box`
- `camel-braintree`
- `camel-caffeine`
- `camel-camunda`
- `camel-cassandraql`
- `camel-catalog`
- `camel-cbor`
- `camel-chatscript`
- `camel-chunk`
- `camel-cli-connector`
- `camel-cli-debug`
- `camel-clickhouse`
- `camel-clickup`
- `camel-cloudevents`
- `camel-cm-sms`
- `camel-coap`
- `camel-cometd`
- `camel-console`
- `camel-consul`
- `camel-core`
- `camel-core-all`
- `camel-core-engine`
- `camel-core-xml`
- `camel-couchbase`
- `camel-couchdb`
- `camel-cron`
- `camel-crypto`
- `camel-crypto-pgp`
- `camel-csv`
- `camel-cxf-common`
- `camel-cxf-rest`
- `camel-cxf-soap`
- `camel-cxf-spring-common`
- `camel-cxf-spring-rest`
- `camel-cxf-spring-soap`
- `camel-cxf-spring-transport`
- `camel-cxf-transport`
- `camel-cyberark-vault`
- `camel-dapr`
- `camel-datasonnet`
- `camel-debezium-common`
- `camel-debezium-db2`
- `camel-debezium-maven-plugin`
- `camel-debezium-mongodb`
- `camel-debezium-mysql`
- `camel-debezium-oracle`
- `camel-debezium-postgres`
- `camel-debezium-sqlserver`
- `camel-debug`
- `camel-dfdl`
- `camel-dhis2`
- `camel-dhis2-api`
- `camel-dhis2-parent`
- `camel-diagram`
- `camel-disruptor`
- `camel-djl`
- `camel-dns`
- `camel-docker`
- `camel-docling`
- `camel-drill`
- `camel-dropbox`
- `camel-dsl-modeline`
- `camel-duckdb`
- `camel-dynamic-router`
- `camel-ehcache`
- `camel-elasticsearch`
- `camel-elasticsearch-rest-client`
- `camel-event`
- `camel-exec`
- `camel-fastjson`
- `camel-fhir`
- `camel-file`
- `camel-file-watch`
- `camel-flatpack`
- `camel-flink`
- `camel-flowable`
- `camel-fop`
- `camel-fory`
- `camel-freemarker`
- `camel-ftp`
- `camel-ftp-common`
- `camel-geocoder`
- `camel-git`
- `camel-github2`
- `camel-google-bigquery`
- `camel-google-calendar`
- `camel-google-common`
- `camel-google-drive`
- `camel-google-firestore`
- `camel-google-functions`
- `camel-google-mail`
- `camel-google-pubsub`
- `camel-google-secret-manager`
- `camel-google-sheets`
- `camel-google-speech-to-text`
- `camel-google-storage`
- `camel-google-text-to-speech`
- `camel-google-vertexai`
- `camel-google-vision`
- `camel-graphql`
- `camel-grok`
- `camel-groovy`
- `camel-grpc`
- `camel-gson`
- `camel-hashicorp-vault`
- `camel-hazelcast`
- `camel-health`
- `camel-hivemq`
- `camel-hl7`
- `camel-http`
- `camel-http-base`
- `camel-http-common`
- `camel-huaweicloud-dms`
- `camel-huaweicloud-frs`
- `camel-huaweicloud-functiongraph`
- `camel-huaweicloud-iam`
- `camel-huaweicloud-imagerecognition`
- `camel-huaweicloud-obs`
- `camel-huaweicloud-smn`
- `camel-huggingface`
- `camel-ibm-cos`
- `camel-ibm-secrets-manager`
- `camel-ibm-watson-discovery`
- `camel-ibm-watson-language`
- `camel-ibm-watson-speech-to-text`
- `camel-ibm-watson-text-to-speech`
- `camel-ibm-watsonx-ai`
- `camel-ibm-watsonx-data`
- `camel-ical`
- `camel-iggy`
- `camel-ignite`
- `camel-infinispan`
- `camel-infinispan-common`
- `camel-infinispan-embedded`
- `camel-influxdb`
- `camel-influxdb2`
- `camel-iso8583`
- `camel-jackson`
- `camel-jackson-avro`
- `camel-jackson-protobuf`
- `camel-jackson3`
- `camel-jackson3-avro`
- `camel-jackson3-protobuf`
- `camel-jackson3xml`
- `camel-jacksonxml`
- `camel-jactl`
- `camel-jandex`
- `camel-jasypt`
- `camel-java-io`
- `camel-java-joor-dsl`
- `camel-javascript`
- `camel-jaxb`
- `camel-jbang-console`
- `camel-jbang-mcp`
- `camel-jbang-plugin-mcp`
- `camel-jbang-plugin-route-parser`
- `camel-jbang-plugin-tui`
- `camel-jbang-plugin-validate`
- `camel-jcache`
- `camel-jcr`
- `camel-jdbc`
- `camel-jetty`
- `camel-jetty-common`
- `camel-jfr`
- `camel-jgroups`
- `camel-jgroups-raft`
- `camel-jira`
- `camel-jms`
- `camel-jmx`
- `camel-jolt`
- `camel-jooq`
- `camel-joor`
- `camel-jpa`
- `camel-jq`
- `camel-jsch`
- `camel-jslt`
- `camel-json-validator`
- `camel-jsonapi`
- `camel-jsonata`
- `camel-jsonb`
- `camel-jsonpath`
- `camel-jsoup`
- `camel-jt400`
- `camel-jta`
- `camel-jte`
- `camel-kafka`
- `camel-kamelet`
- `camel-kamelet-main-support`
- `camel-keycloak`
- `camel-knative`
- `camel-knative-api`
- `camel-knative-http`
- `camel-kserve`
- `camel-kubernetes`
- `camel-kudu`
- `camel-langchain4j-agent`
- `camel-langchain4j-chat`
- `camel-langchain4j-core`
- `camel-langchain4j-embeddings`
- `camel-langchain4j-embeddingstore`
- `camel-langchain4j-ingest`
- `camel-langchain4j-tokenizer`
- `camel-langchain4j-web-search`
- `camel-launcher-container`
- `camel-ldap`
- `camel-ldif`
- `camel-lra`
- `camel-lucene`
- `camel-lumberjack`
- `camel-lzf`
- `camel-mail`
- `camel-mail-microsoft-oauth`
- `camel-main`
- `camel-management`
- `camel-mapstruct`
- `camel-master`
- `camel-maven-plugin`
- `camel-mcp-server`
- `camel-mcp-server-api`
- `camel-mdc`
- `camel-metrics`
- `camel-micrometer`
- `camel-micrometer-observability`
- `camel-micrometer-prometheus`
- `camel-microprofile-config`
- `camel-microprofile-fault-tolerance`
- `camel-microprofile-health`
- `camel-milo`
- `camel-milvus`
- `camel-mina`
- `camel-mina-sftp`
- `camel-minio`
- `camel-mllp`
- `camel-mongodb`
- `camel-mongodb-gridfs`
- `camel-mustache`
- `camel-mvel`
- `camel-mybatis`
- `camel-nats`
- `camel-neo4j`
- `camel-netty`
- `camel-netty-http`
- `camel-oaipmh`
- `camel-oauth`
- `camel-observability-services`
- `camel-observation`
- `camel-ocsf`
- `camel-ognl`
- `camel-olingo2`
- `camel-olingo2-api`
- `camel-olingo4`
- `camel-olingo4-api`
- `camel-once`
- `camel-opa`
- `camel-openai`
- `camel-openapi-java`
- `camel-openapi-rest-dsl-generator`
- `camel-openapi-validator`
- `camel-opensearch`
- `camel-openstack`
- `camel-opentelemetry`
- `camel-opentelemetry-metrics`
- `camel-opentelemetry2`
- `camel-optaplanner`
- `camel-paho`
- `camel-paho-mqtt5`
- `camel-parquet-avro`
- `camel-pdf`
- `camel-pg-replication-slot`
- `camel-pgevent`
- `camel-pgvector`
- `camel-pinecone`
- `camel-platform-http`
- `camel-platform-http-jolokia`
- `camel-platform-http-main`
- `camel-platform-http-vertx`
- `camel-plc4x`
- `camel-pqc`
- `camel-printer`
- `camel-protobuf`
- `camel-pubnub`
- `camel-pulsar`
- `camel-python`
- `camel-python3`
- `camel-qdrant`
- `camel-quartz`
- `camel-quickfix`
- `camel-quickjs`
- `camel-reactive-streams`
- `camel-reactor`
- `camel-redis`
- `camel-resilience4j`
- `camel-resilience4j-micrometer`
- `camel-resourceresolver-github`
- `camel-rest`
- `camel-rest-openapi`
- `camel-rest-postman`
- `camel-restdsl-openapi-plugin`
- `camel-robotframework`
- `camel-rocketmq`
- `camel-rss`
- `camel-rxjava`
- `camel-salesforce`
- `camel-salesforce-codegen`
- `camel-salesforce-maven-plugin`
- `camel-sap-netweaver`
- `camel-saxon`
- `camel-schematron`
- `camel-seda`
- `camel-servicenow`
- `camel-servicenow-maven-plugin`
- `camel-servlet`
- `camel-shell`
- `camel-shiro`
- `camel-sjms`
- `camel-sjms2`
- `camel-slack`
- `camel-smb`
- `camel-smooks`
- `camel-smpp`
- `camel-snakeyaml`
- `camel-snmp`
- `camel-soap`
- `camel-solr`
- `camel-spiffe`
- `camel-splunk-hec`
- `camel-spring`
- `camel-spring-ai-chat`
- `camel-spring-ai-embeddings`
- `camel-spring-ai-image`
- `camel-spring-ai-vector-store`
- `camel-spring-batch`
- `camel-spring-cloud-config`
- `camel-spring-jdbc`
- `camel-spring-ldap`
- `camel-spring-main`
- `camel-spring-rabbitmq`
- `camel-spring-redis`
- `camel-spring-security`
- `camel-spring-ws`
- `camel-spring-xml`
- `camel-sql`
- `camel-ssh`
- `camel-state-store`
- `camel-stax`
- `camel-stitch`
- `camel-stream`
- `camel-streamcaching-test`
- `camel-stringtemplate`
- `camel-stripe`
- `camel-stub`
- `camel-swift`
- `camel-syslog`
- `camel-tahu`
- `camel-tarfile`
- `camel-telegram`
- `camel-telemetry`
- `camel-telemetry-dev`
- `camel-tensorflow-serving`
- `camel-test-infra-all`
- `camel-test-infra-artemis`
- `camel-test-infra-core`
- `camel-test-infra-smb`
- `camel-test-junit5`
- `camel-test-junit6`
- `camel-test-main-junit5`
- `camel-test-main-junit6`
- `camel-test-spring-junit5`
- `camel-test-spring-junit6`
- `camel-thrift`
- `camel-thymeleaf`
- `camel-tika`
- `camel-toon`
- `camel-tracing`
- `camel-twilio`
- `camel-twitter`
- `camel-ubl`
- `camel-undertow`
- `camel-undertow-spring-security`
- `camel-univocity-parsers`
- `camel-velocity`
- `camel-vertx`
- `camel-vertx-http`
- `camel-vertx-websocket`
- `camel-wasm`
- `camel-weather`
- `camel-weaviate`
- `camel-web3j`
- `camel-webhook`
- `camel-whatsapp`
- `camel-wordpress`
- `camel-workday`
- `camel-xchange`
- `camel-xj`
- `camel-xml-io`
- `camel-xml-io-dsl`
- `camel-xml-jaxb`
- `camel-xml-jaxb-dsl`
- `camel-xml-jaxb-dsl-test-definition`
- `camel-xml-jaxb-dsl-test-spring`
- `camel-xmlsecurity`
- `camel-xmpp`
- `camel-xpath`
- `camel-xslt-saxon`
- `camel-yaml-dsl-common`
- `camel-yaml-dsl-validator`
- `camel-yaml-io`
- `camel-zendesk`
- `camel-zip-deflater`
- `camel-zipfile`
- `camel-zookeeper`
- `camel-zookeeper-master`
- `docs`
</details>
<details><summary>Modules with tests skipped (25)</summary>
- `apache-camel`
- `camel-allcomponents`
- `camel-catalog-console`
- `camel-catalog-maven`
- `camel-catalog-suggest`
- `camel-componentdsl`
- `camel-endpointdsl`
- `camel-endpointdsl-support`
- `camel-itest`
- `camel-jbang-core`
- `camel-jbang-it`
- `camel-jbang-main`
- `camel-jbang-plugin-edit`
- `camel-jbang-plugin-generate`
- `camel-jbang-plugin-kubernetes`
- `camel-jbang-plugin-test`
- `camel-kamelet-main`
- `camel-launcher`
- `camel-report-maven-plugin`
- `camel-route-parser`
- `camel-yaml-dsl`
- `camel-yaml-dsl-deserializers`
- `camel-yaml-dsl-maven-plugin`
- `coverage`
- `dummy-component`
</details>
> :information_source: Shadow mode — Scalpel observes but does not affect test execution. [Learn more](https://github.com/maveniverse/scalpel)
</details>
:warning: **Some tests are disabled on GitHub Actions** (`@DisabledIfSystemProperty(named = "ci.env.name")`) and require manual verification:
- `components/camel-netty`: 1 test(s) disabled on GitHub Actions
<details><summary>Build reactor — dependencies compiled but only changed modules were tested (9 modules, 1m 34s total)</summary>
**Total reactor time:** 1m 34s
| Module | Duration | Status |
| --- | --- | --- |
| Camel :: Catalog :: Camel Catalog | 23.6s | SUCCESS |
| Camel :: UniVocity Parsers | 21.8s | SUCCESS |
| Camel :: Docs | 17.5s | SUCCESS |
| Camel :: Cometd | 11.9s | SUCCESS |
| Camel :: Seda | 6.2s | SUCCESS |
| Camel :: XPath | 6.2s | SUCCESS |
| Camel :: YAML DSL :: Validator | 4.7s | SUCCESS |
| Camel :: Core Engine | 2.3s | SUCCESS |
| Camel :: Netty | n/a | |
**Top 20 slowest modules:**
- `Camel :: Catalog :: Camel Catalog` (23.6s)
- `Camel :: UniVocity Parsers` (21.8s)
- `Camel :: Docs` (17.5s)
- `Camel :: Cometd` (11.9s)
- `Camel :: Seda` (6.2s)
- `Camel :: XPath` (6.2s)
- `Camel :: YAML DSL :: Validator` (4.7s)
- `Camel :: Core Engine` (2.3s)
</details>
---
:gear: [View full build and test results](https://github.com/apache/camel/actions/runs/35133686139)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org