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

Reply via email to