This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit cb2ec145303f9a4719d37098a2fe47ae1a489c72 Author: Luigi De Masi <[email protected]> AuthorDate: Wed Sep 30 13:17:37 2026 +0200 CAMEL-25138: Streamline semantic XML discovery Limit declaration detection to top-level elements, skip route subtrees and stop at the closing routes tag while allowing declarations after routes. Reuse lazily discovered application loaders and refresh discovery at startup, reload and registry replacement instead of scanning on each check. Remove the migration-guide note for the new component and retain its component documentation. Cover discovery reuse, refresh and late XML declarations with regression tests. Co-authored-by: Codex <[email protected]> Signed-off-by: Luigi De Masi <[email protected]> --- .../camel/catalog/docs/semantic-language.adoc | 3 +- .../src/main/docs/semantic-language.adoc | 3 +- .../camel/semantic/SemanticReloadPlugin.java | 8 +- .../apache/camel/semantic/SemanticXmlLoader.java | 51 ++++++++- .../camel/semantic/SemanticXmlLoaderTest.java | 124 +++++++++++++++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 7 -- 6 files changed, 180 insertions(+), 16 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc index 41c3797b928b..c654e13feb35 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc @@ -147,7 +147,8 @@ xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`). Camel discovers automatically at startup: use ordinary `*.xml` files without registering a loader or changing the Camel core model. XML documents without semantic declarations are handled by the standard XML loader, including its bean and route configuration support. XML loaders registered by the -application before route loading retain precedence over automatic discovery. +application before route resource discovery retain precedence over automatic discovery. +Loader discovery is refreshed when the context starts or routes are reloaded. Filenames may contain dots, such as `my.tickets.xml`. Routes retain their original resource locations and line numbers for debugging and error messages. diff --git a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc index 41c3797b928b..c654e13feb35 100644 --- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc +++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc @@ -147,7 +147,8 @@ xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`). Camel discovers automatically at startup: use ordinary `*.xml` files without registering a loader or changing the Camel core model. XML documents without semantic declarations are handled by the standard XML loader, including its bean and route configuration support. XML loaders registered by the -application before route loading retain precedence over automatic discovery. +application before route resource discovery retain precedence over automatic discovery. +Loader discovery is refreshed when the context starts or routes are reloaded. Filenames may contain dots, such as `my.tickets.xml`. Routes retain their original resource locations and line numbers for debugging and error messages. diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java index 1fb38ad3e502..1b4374154712 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java @@ -26,7 +26,7 @@ import org.apache.camel.support.service.ServiceHelper; /** Installs optional XML declaration support and removes definitions from deleted files before route reload. */ public class SemanticReloadPlugin implements ContextServicePlugin { - private RoutesBuilderLoader xmlLoader; + private SemanticXmlLoader xmlLoader; private LifecycleStrategy lifecycle; @Override @@ -37,6 +37,9 @@ public class SemanticReloadPlugin implements ContextServicePlugin { public void onContextInitializing(CamelContext camelContext) { // Applications can replace the registry after the context's eager build phase. installXmlLoader(camelContext); + if (xmlLoader != null) { + xmlLoader.resetLoaderDiscovery(); + } } }; context.addLifecycleStrategy(lifecycle); @@ -82,6 +85,9 @@ public class SemanticReloadPlugin implements ContextServicePlugin { @Override public void onReload(CamelContext context) { + if (xmlLoader != null) { + xmlLoader.resetLoaderDiscovery(); + } SemanticQuestions questions = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class); if (questions != null) { questions.removeDeletedResources(); diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java index e5d829e9e62b..b3be7899bd4a 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java @@ -17,14 +17,17 @@ package org.apache.camel.semantic; import java.io.InputStream; +import java.util.List; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import org.apache.camel.RoutesBuilder; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.builder.RouteBuilderLifecycleStrategy; +import org.apache.camel.spi.Registry; import org.apache.camel.spi.Resource; import org.apache.camel.spi.RoutesBuilderLoader; import org.apache.camel.support.CachedResource; @@ -38,6 +41,8 @@ final class SemanticXmlLoader extends RoutesBuilderLoaderSupport { static final String REGISTRY_KEY = "semantic-xml-routes-loader"; private RoutesBuilderLoader delegate; private final SemanticXmlRoutesBuilderLoader semantic = new SemanticXmlRoutesBuilderLoader(); + private Registry loaderRegistry; + private List<RoutesBuilderLoader> otherLoaders = List.of(); @Override public String getSupportedExtension() { @@ -46,10 +51,23 @@ final class SemanticXmlLoader extends RoutesBuilderLoaderSupport { @Override public boolean isSupportedExtension(String extension) { - // DefaultCamelContext may build before application beans are bound. Give later custom loaders precedence too. return ("xml".equals(extension) || extension.endsWith(".xml")) - && getCamelContext().getRegistry().findByType(RoutesBuilderLoader.class).stream() - .noneMatch(loader -> loader != this && loader.isSupportedExtension(extension)); + && otherLoaders().stream().noneMatch(loader -> loader.isSupportedExtension(extension)); + } + + private synchronized List<RoutesBuilderLoader> otherLoaders() { + Registry registry = getCamelContext().getRegistry(); + if (loaderRegistry != registry) { + // Discover lazily: the context may build before application loaders are registered. + otherLoaders = registry.findByType(RoutesBuilderLoader.class).stream().filter(loader -> loader != this).toList(); + loaderRegistry = registry; + } + return otherLoaders; + } + + synchronized void resetLoaderDiscovery() { + loaderRegistry = null; + otherLoaders = List.of(); } @Override @@ -107,6 +125,7 @@ final class SemanticXmlLoader extends RoutesBuilderLoaderSupport { protected void doStop() throws Exception { ServiceHelper.stopAndShutdownServices(delegate, semantic); delegate = null; + resetLoaderDiscovery(); super.doStop(); } @@ -141,11 +160,19 @@ final class SemanticXmlLoader extends RoutesBuilderLoaderSupport { if (!"routes".equals(reader.getLocalName())) { return false; } - } else if (depth == 2 && "semantic".equals(reader.getLocalName())) { - return true; + } else if (depth == 2) { + if ("semantic".equals(reader.getLocalName())) { + return true; + } + // Only direct children can declare questions. Consume nested route content without + // inspecting names or namespaces, then continue looking for declarations after routes. + skipSubtree(reader); + depth--; } } else if (event == XMLStreamConstants.END_ELEMENT) { - depth--; + if (--depth == 0) { + return false; + } } } return false; @@ -154,4 +181,16 @@ final class SemanticXmlLoader extends RoutesBuilderLoaderSupport { } } } + + private static void skipSubtree(XMLStreamReader reader) throws XMLStreamException { + int depth = 1; + while (depth != 0) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) { + depth++; + } else if (event == XMLStreamConstants.END_ELEMENT) { + depth--; + } + } + } } diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java new file mode 100644 index 000000000000..2829377894d7 --- /dev/null +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java @@ -0,0 +1,124 @@ +/* + * 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.semantic; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.camel.dsl.xml.io.XmlRoutesBuilderLoader; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.spi.ContextServiceLoaderPluginResolver; +import org.apache.camel.spi.RoutesBuilderLoader; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.ResourceHelper; +import org.apache.camel.support.SimpleRegistry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class SemanticXmlLoaderTest { + @Test + void loaderDiscoveryIsReusedAndRefreshedOnReload() throws Exception { + AtomicInteger lookups = new AtomicInteger(); + SimpleRegistry registry = new SimpleRegistry() { + @Override + public <T> Set<T> findByType(Class<T> type) { + if (type == RoutesBuilderLoader.class) { + lookups.incrementAndGet(); + } + return super.findByType(type); + } + }; + try (var context = new DefaultCamelContext(registry)) { + context.start(); + SemanticXmlLoader loader = registry.lookupByNameAndType(SemanticXmlLoader.REGISTRY_KEY, SemanticXmlLoader.class); + lookups.set(0); + for (String extension : new String[] { "xml", "tickets.xml", "semantic.xml", "xml" }) { + assertThat(loader.isSupportedExtension(extension)).isTrue(); + } + assertThat(loader.isSupportedExtension("yaml")).isFalse(); + assertThat(lookups).hasValue(1); + + registry.bind("customXml", new XmlRoutesBuilderLoader()); + var plugins = context.getCamelContextExtension().getContextPlugin(ContextServiceLoaderPluginResolver.class); + plugins.onReload(); + assertThat(loader.isSupportedExtension("xml")).isFalse(); + assertThat(loader.isSupportedExtension("xml")).isFalse(); + assertThat(lookups).hasValue(2); + + registry.unbind("customXml"); + plugins.onReload(); + assertThat(loader.isSupportedExtension("xml")).isTrue(); + assertThat(loader.isSupportedExtension("tickets.xml")).isTrue(); + assertThat(lookups).hasValue(3); + } + } + + @Test + void startupRefreshesDiscoveryAfterApplicationRegistersLoader() throws Exception { + try (var context = new DefaultCamelContext()) { + SemanticXmlLoader loader = context.getRegistry() + .lookupByNameAndType(SemanticXmlLoader.REGISTRY_KEY, SemanticXmlLoader.class); + assertThat(loader.isSupportedExtension("xml")).isTrue(); + XmlRoutesBuilderLoader custom = new XmlRoutesBuilderLoader(); + context.getRegistry().bind("customXml", custom); + + context.start(); + + assertThat(loader.isSupportedExtension("xml")).isFalse(); + assertThat(PluginHelper.getRoutesLoader(context).getRoutesLoader("xml")).isSameAs(custom); + } + } + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void nestedRoutesDoNotHideFollowingDeclarations(boolean declarations) throws Exception { + String routes = """ + <routes> + <route id="nested"> + <from uri="direct:nested"/> + <choice> + <when><simple>${body} != null</simple> + <setBody><constant><![CDATA[<semantic/>]]></constant></setBody> + </when> + </choice> + </route> + <route id="other"><from uri="direct:other"/><log message="Hello"/></route> + %s + </routes> + """.formatted(declarations ? """ + <semantic> + <question name="urgent" type="boolean"><instructions>Urgent?</instructions></question> + </semantic> + """ : ""); + try (var context = new DefaultCamelContext()) { + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("nested.routes.xml", routes)); + context.start(); + assertThat(context.getRouteDefinitions()).hasSize(2); + if (declarations) { + assertThat(SemanticQuestions.get(context).get("urgent").getInstructions()).isEqualTo("Urgent?"); + } else { + assertThat(context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)).isNull(); + } + try (var template = context.createProducerTemplate()) { + assertThat(template.requestBody("direct:nested", "hello", String.class)).isEqualTo("<semantic/>"); + } + } + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc index bdef0ae31380..b62a9a431e19 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc @@ -3978,13 +3978,6 @@ JAXP external-access model and remain resolvable. Stylesheet `xsl:include` / `xs are resolved at compile time (they are part of the route definition, authored by the route author) and are unaffected by this change. -=== camel-semantic - automatic XML route loading - -When `camel-semantic` and the XML DSL are on the classpath, Camel now automatically detects -semantic declarations in ordinary XML route resources. XML documents without declarations -continue through the standard XML loader. Explicitly registered XML loaders retain precedence. -This changes route resource discovery only; Spring XML application-context parsing is unaffected. - == ThrottlingExceptionRoutePolicy `ThrottlingExceptionRoutePolicy.setKeepOpen(true)` now opens the circuit immediately and synchronously (the consumer
