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 44a856230dec8637a5d4bb910477164f7cb50f62
Author: Luigi De Masi <[email protected]>
AuthorDate: Wed Jun 17 13:52:45 2026 +0200

    CAMEL-23775: Add YAML deserializer resolver provider SPI
    
    Introduce a YamlDeserializerResolverProvider SPI so runtimes can control how
    YAML deserializer resolvers are discovered.
    
    The default provider preserves the existing classpath service discovery 
behavior,
    while runtimes such as Camel Quarkus or Spring Boot can install a 
CamelContext
    plugin to provide precomputed resolvers or avoid runtime classpath scanning.
    
    Also document the provider hook and add tests covering provider-based 
discovery,
    provider replacement of default classpath discovery, and registry resolver
    discovery with a custom provider installed.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc    |   4 +-
 .../camel-yaml-dsl-validator-maven-plugin.adoc     |   8 +-
 .../DefaultYamlDeserializerResolverProvider.java   | 141 +++++++
 .../yaml/common/YamlDeserializationContext.java    | 217 ++++------
 .../dsl/yaml/common/YamlDeserializerResolver.java  |   6 +
 .../common/YamlDeserializerResolverProvider.java   |  45 ++
 .../camel-yaml-dsl-validator-maven-plugin.adoc     |   8 +-
 .../dsl/yaml/validator/YamlValidatorTest.java      |   7 +
 .../camel-yaml-dsl/src/main/docs/yaml-dsl.adoc     |  42 +-
 .../YamlDeserializerResolverDiscoveryTest.groovy   | 460 ++++++++++++++++++++-
 10 files changed, 786 insertions(+), 152 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index cf0840c2d10c..38da59bba86d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -349,7 +349,7 @@ documentation for details.
 The `YamlValidator` class now accepts a `boolean canonical` constructor 
parameter to validate against the
 canonical schema.
 
-A new `camel validate normalize` command has been added to Camel JBang. It 
rewrites YAML routes from the
+A new `camel validate normalize` command has been added to Camel CLI. It 
rewrites YAML routes from the
 classic (shorthand) form to the canonical (explicit) form. The `camel validate 
yaml` command also
 supports a new `--canonical` flag to validate against the canonical schema.
 
@@ -365,7 +365,7 @@ to contribute custom YAML route step names automatically 
when the module is on t
 Registry-provided resolvers remain supported.
 Custom step names are runtime extensions and are not included in the generated 
YAML DSL JSON schemas unless schema
 generation is extended separately.
-See xref:components:others:yaml-dsl.adoc#_extending_yaml_route_steps[Extending 
YAML route steps] for details.
+See xref:components:others:yaml-dsl.adoc[YAML DSL] for details.
 
 === camel-a2a (new, preview)
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-yaml-dsl-validator-maven-plugin.adoc
 
b/docs/user-manual/modules/ROOT/pages/camel-yaml-dsl-validator-maven-plugin.adoc
index f235056945cb..f1cbe3439110 100644
--- 
a/docs/user-manual/modules/ROOT/pages/camel-yaml-dsl-validator-maven-plugin.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/camel-yaml-dsl-validator-maven-plugin.adoc
@@ -6,12 +6,12 @@ The Camel YAML DSL Validator Maven Plugin supports the 
following goals
 
 == camel-yaml-dsl-validator:validate
 
-For validating the YAML routes for syntax errors according to the spec.
+For validating YAML routes against the generated YAML DSL JSON Schema.
 
-The validator uses the generated YAML DSL JSON Schema.
 Runtime extensions such as custom `YamlDeserializerResolver` implementations 
are not added to this schema, so the
-validator does not accept module-provided custom YAML step names. For those 
routes, use runtime route loading tests in
-addition to this schema validation.
+`validate` goal does not accept module-provided custom YAML step names. The 
Maven plugin does not run Camel's YAML route
+loader and does not load custom resolvers. For those routes, use runtime route 
loading tests in addition to this schema
+validation.
 
 Then you can run the `validate` goal from the command line or from within your 
Java editor such as IDEA or Eclipse.
 
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/DefaultYamlDeserializerResolverProvider.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/DefaultYamlDeserializerResolverProvider.java
new file mode 100644
index 000000000000..cf55c4bd6343
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/DefaultYamlDeserializerResolverProvider.java
@@ -0,0 +1,141 @@
+/*
+ * 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.common;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
+
+/**
+ * Default YAML deserializer resolver provider that discovers resolver class 
names from the classpath.
+ */
+public class DefaultYamlDeserializerResolverProvider implements 
YamlDeserializerResolverProvider {
+
+    @Override
+    public Map<String, YamlDeserializerResolver> findResolvers(CamelContext 
camelContext) {
+        Map<String, YamlDeserializerResolver> resolvers = new 
LinkedHashMap<>();
+        for (String resolverClassName : findResolverClassNames(camelContext)) {
+            resolvers.put(resolverClassName, newResolver(camelContext, 
resolverClassName));
+        }
+        return resolvers;
+    }
+
+    private YamlDeserializerResolver newResolver(CamelContext camelContext, 
String resolverClassName) {
+        try {
+            Class<YamlDeserializerResolver> resolverType
+                    = 
camelContext.getClassResolver().resolveMandatoryClass(resolverClassName,
+                            YamlDeserializerResolver.class);
+            return camelContext.getInjector().newInstance(resolverType, false);
+        } catch (Exception e) {
+            String message = "Error loading YAML deserializer resolver " + 
resolverClassName + " from "
+                             + YamlDeserializerResolver.RESOURCE_PATH;
+            throw new YamlDeserializationException(message, e);
+        }
+    }
+
+    private List<String> findResolverClassNames(CamelContext camelContext) {
+        Set<String> resolverClassNames = new LinkedHashSet<>();
+
+        for (URL resolverResource : findResolverResources(camelContext)) {
+            
resolverClassNames.addAll(readResolverClassNames(resolverResource));
+        }
+
+        List<String> sortedResolverClassNames = new 
ArrayList<>(resolverClassNames);
+        sortedResolverClassNames.sort(String::compareTo);
+        return sortedResolverClassNames;
+    }
+
+    private List<String> readResolverClassNames(URL resolverResource) {
+        List<String> resolverClassNames = new ArrayList<>();
+
+        try (BufferedReader reader
+                = new BufferedReader(new 
InputStreamReader(resolverResource.openStream(), StandardCharsets.UTF_8))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                String resolverClassName = parseResolverClassName(line);
+                if (!resolverClassName.isEmpty()) {
+                    resolverClassNames.add(resolverClassName);
+                }
+            }
+        } catch (IOException e) {
+            throw new YamlDeserializationException(
+                    "Error reading YAML deserializer resolver resource from "
+                                                   + 
YamlDeserializerResolver.RESOURCE_PATH + " ("
+                                                   + 
safeResourceDescription(resolverResource) + ")",
+                    e);
+        }
+
+        return resolverClassNames;
+    }
+
+    private static String safeResourceDescription(URL resolverResource) {
+        if (resolverResource == null || resolverResource.getProtocol() == 
null) {
+            return "unknown resource";
+        }
+        return resolverResource.getProtocol() + " resource";
+    }
+
+    private static String parseResolverClassName(String line) {
+        int comment = line.indexOf('#');
+        String uncommentedLine = comment != -1 ? line.substring(0, comment) : 
line;
+        return uncommentedLine.trim();
+    }
+
+    private Set<URL> findResolverResources(CamelContext camelContext) {
+        Set<URL> resolverResources = new LinkedHashSet<>();
+
+        try {
+            addResolverResources(resolverResources,
+                    
camelContext.getClassResolver().loadAllResourcesAsURL(YamlDeserializerResolver.RESOURCE_PATH));
+            addResolverResources(resolverResources, 
camelContext.getApplicationContextClassLoader());
+            for (ClassLoader classLoader : 
camelContext.getClassResolver().getClassLoaders()) {
+                addResolverResources(resolverResources, classLoader);
+            }
+        } catch (IOException e) {
+            throw new YamlDeserializationException(
+                    "Error locating YAML deserializer resolvers from " + 
YamlDeserializerResolver.RESOURCE_PATH, e);
+        }
+
+        return resolverResources;
+    }
+
+    private static void addResolverResources(Set<URL> resolverResources, 
ClassLoader classLoader) throws IOException {
+        if (classLoader != null) {
+            addResolverResources(resolverResources, 
classLoader.getResources(YamlDeserializerResolver.RESOURCE_PATH));
+        }
+    }
+
+    private static void addResolverResources(Set<URL> resolverResources, 
Enumeration<URL> resources) {
+        if (resources != null) {
+            while (resources.hasMoreElements()) {
+                resolverResources.add(resources.nextElement());
+            }
+        }
+    }
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializationContext.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializationContext.java
index f51ce2f2c4b3..cb95ebf8e3b7 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializationContext.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializationContext.java
@@ -16,22 +16,13 @@
  */
 package org.apache.camel.dsl.yaml.common;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Enumeration;
 import java.util.HashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.CamelContextAware;
@@ -151,119 +142,59 @@ public class YamlDeserializationContext extends 
StandardConstructor implements C
             return;
         }
 
-        addResolverEntries(loadResolversFromClasspath());
-        addResolverEntries(loadResolversFromRegistry());
+        List<ResolverEntry> resolverEntries = new ArrayList<>();
+        resolverEntries.addAll(loadResolversFromProvider());
+        resolverEntries.addAll(loadResolversFromRegistry());
+
+        addResolverEntries(resolverEntries);
         resolversLoaded = true;
     }
 
     @Override
     public void stop() {
         this.constructors.clear();
-        this.resolvers.removeIf(entry -> entry.source == 
ResolverSource.CLASSPATH || entry.source == ResolverSource.REGISTRY);
+        this.resolvers.removeIf(entry -> entry.source == 
ResolverSource.PROVIDER || entry.source == ResolverSource.REGISTRY);
         this.resolversLoaded = false;
     }
 
-    private List<ResolverEntry> loadResolversFromClasspath() {
-        List<String> resolverClassNames;
-        resolverClassNames = findResolverClassNames();
+    private List<ResolverEntry> loadResolversFromProvider() {
+        YamlDeserializerResolverProvider provider = getResolverProvider();
+        return loadResolverEntries(provider.findResolvers(getCamelContext()), 
ResolverSource.PROVIDER);
+    }
 
-        List<ResolverEntry> discoveredResolvers = new 
ArrayList<>(resolverClassNames.size());
-        for (String resolverClassName : resolverClassNames) {
-            discoveredResolvers
-                    .add(new ResolverEntry(newResolver(resolverClassName), 
ResolverSource.CLASSPATH, resolverClassName));
-        }
-        discoveredResolvers.sort(this::compareResolverEntries);
-        return discoveredResolvers;
+    private YamlDeserializerResolverProvider getResolverProvider() {
+        YamlDeserializerResolverProvider provider = 
getCamelContext().getCamelContextExtension()
+                .getContextPlugin(YamlDeserializerResolverProvider.class);
+        return provider != null ? provider : new 
DefaultYamlDeserializerResolverProvider();
     }
 
     private List<ResolverEntry> loadResolversFromRegistry() {
-        Map<String, YamlDeserializerResolver> resolvers
-                = 
getCamelContext().getRegistry().findByTypeWithName(YamlDeserializerResolver.class);
-        List<ResolverEntry> answer = new ArrayList<>(resolvers.size());
-        for (Map.Entry<String, YamlDeserializerResolver> entry : 
resolvers.entrySet()) {
-            answer.add(new ResolverEntry(entry.getValue(), 
ResolverSource.REGISTRY, entry.getKey()));
-        }
-        answer.sort(this::compareResolverEntries);
-        return answer;
+        return 
loadResolverEntries(getCamelContext().getRegistry().findByTypeWithName(YamlDeserializerResolver.class),
+                ResolverSource.REGISTRY);
     }
 
-    private YamlDeserializerResolver newResolver(String resolverClassName) {
-        try {
-            Class<YamlDeserializerResolver> type
-                    = 
getCamelContext().getClassResolver().resolveMandatoryClass(resolverClassName,
-                            YamlDeserializerResolver.class);
-            YamlDeserializerResolver resolver = 
getCamelContext().getInjector().newInstance(type, false);
-            CamelContextAware.trySetCamelContext(resolver, getCamelContext());
-            return resolver;
-        } catch (Exception e) {
-            String message = "Error loading YAML deserializer resolver " + 
resolverClassName + " from "
-                             + YamlDeserializerResolver.RESOURCE_PATH;
+    private List<ResolverEntry> loadResolverEntries(
+            Map<String, YamlDeserializerResolver> resolvers, ResolverSource 
source) {
+        if (resolvers == null) {
             throw new YamlDeserializationException(
-                    message,
-                    e);
+                    "YAML deserializer resolver " + source + " returned a null 
resolver map");
         }
-    }
 
-    private List<String> findResolverClassNames() {
-        Set<String> resolverClassNames = new LinkedHashSet<>();
-
-        for (URL url : findResolverResources()) {
-            try (BufferedReader reader
-                    = new BufferedReader(new 
InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
-                String line;
-                while ((line = reader.readLine()) != null) {
-                    int comment = line.indexOf('#');
-                    if (comment != -1) {
-                        line = line.substring(0, comment);
-                    }
-                    line = line.trim();
-                    if (!line.isEmpty() && !line.startsWith("#")) {
-                        resolverClassNames.add(line);
-                    }
-                }
-            } catch (IOException e) {
+        List<ResolverEntry> resolverEntries = new 
ArrayList<>(resolvers.size());
+        for (Map.Entry<String, YamlDeserializerResolver> entry : 
resolvers.entrySet()) {
+            if (entry.getKey() == null) {
                 throw new YamlDeserializationException(
-                        "Error reading YAML deserializer resolver resource " + 
url + " from "
-                                                       + 
YamlDeserializerResolver.RESOURCE_PATH,
-                        e);
+                        "YAML deserializer resolver " + source + " returned a 
null resolver name");
             }
-        }
-
-        List<String> sortedResolverClassNames = new 
ArrayList<>(resolverClassNames);
-        Collections.sort(sortedResolverClassNames);
-        return sortedResolverClassNames;
-    }
-
-    private Set<URL> findResolverResources() {
-        Set<URL> answer = new LinkedHashSet<>();
-
-        try {
-            addResolverResources(answer,
-                    
getCamelContext().getClassResolver().loadAllResourcesAsURL(YamlDeserializerResolver.RESOURCE_PATH));
-            addResolverResources(answer, 
getCamelContext().getApplicationContextClassLoader());
-            for (ClassLoader classLoader : 
getCamelContext().getClassResolver().getClassLoaders()) {
-                addResolverResources(answer, classLoader);
-            }
-        } catch (IOException e) {
-            throw new YamlDeserializationException(
-                    "Error locating YAML deserializer resolvers from " + 
YamlDeserializerResolver.RESOURCE_PATH, e);
-        }
-
-        return answer;
-    }
-
-    private static void addResolverResources(Set<URL> answer, ClassLoader 
classLoader) throws IOException {
-        if (classLoader != null) {
-            addResolverResources(answer, 
classLoader.getResources(YamlDeserializerResolver.RESOURCE_PATH));
-        }
-    }
-
-    private static void addResolverResources(Set<URL> answer, Enumeration<URL> 
resources) {
-        if (resources != null) {
-            while (resources.hasMoreElements()) {
-                answer.add(resources.nextElement());
+            YamlDeserializerResolver resolver = entry.getValue();
+            if (resolver == null) {
+                throw new YamlDeserializationException(
+                        "YAML deserializer resolver " + source + " returned a 
null resolver for " + entry.getKey());
             }
+            CamelContextAware.trySetCamelContext(resolver, getCamelContext());
+            resolverEntries.add(new ResolverEntry(resolver, source, 
entry.getKey()));
         }
+        return resolverEntries;
     }
 
     private void addResolver(YamlDeserializerResolver resolver, ResolverSource 
source, String name) {
@@ -282,36 +213,36 @@ public class YamlDeserializationContext extends 
StandardConstructor implements C
         this.resolvers.sort(this::compareResolverEntries);
     }
 
-    private int compareResolverEntries(ResolverEntry entry1, ResolverEntry 
entry2) {
-        int result = compareBuiltInBoundary(entry1, entry2);
+    private int compareResolverEntries(ResolverEntry first, ResolverEntry 
second) {
+        int result = compareBuiltInBoundary(first, second);
         if (result != 0) {
             return result;
         }
 
-        result = OrderedComparator.get().compare(entry1.resolver, 
entry2.resolver);
+        result = OrderedComparator.get().compare(first.resolver, 
second.resolver);
         if (result != 0) {
             return result;
         }
 
-        result = Integer.compare(entry1.source.rank, entry2.source.rank);
+        result = Integer.compare(first.source.rank, second.source.rank);
         if (result != 0) {
             return result;
         }
 
-        result = 
entry1.resolver.getClass().getName().compareTo(entry2.resolver.getClass().getName());
+        result = 
first.resolver.getClass().getName().compareTo(second.resolver.getClass().getName());
         if (result != 0) {
             return result;
         }
 
-        return entry1.name.compareTo(entry2.name);
+        return first.name.compareTo(second.name);
     }
 
-    private int compareBuiltInBoundary(ResolverEntry entry1, ResolverEntry 
entry2) {
-        if (entry1.source == ResolverSource.BUILT_IN && entry2.source != 
ResolverSource.BUILT_IN) {
-            return entry2.resolver.getOrder() < 
YamlDeserializerResolver.ORDER_DEFAULT ? 1 : -1;
+    private int compareBuiltInBoundary(ResolverEntry first, ResolverEntry 
second) {
+        if (first.isBuiltIn() && !second.isBuiltIn()) {
+            return second.overridesBuiltIns() ? 1 : -1;
         }
-        if (entry1.source != ResolverSource.BUILT_IN && entry2.source == 
ResolverSource.BUILT_IN) {
-            return entry1.resolver.getOrder() < 
YamlDeserializerResolver.ORDER_DEFAULT ? -1 : 1;
+        if (!first.isBuiltIn() && second.isBuiltIn()) {
+            return first.overridesBuiltIns() ? -1 : 1;
         }
         return 0;
     }
@@ -326,13 +257,21 @@ public class YamlDeserializationContext extends 
StandardConstructor implements C
             this.source = source;
             this.name = name != null ? name : resolver.getClass().getName();
         }
+
+        private boolean isBuiltIn() {
+            return source == ResolverSource.BUILT_IN;
+        }
+
+        private boolean overridesBuiltIns() {
+            return resolver.getOrder() < 
YamlDeserializerResolver.ORDER_DEFAULT;
+        }
     }
 
     private enum ResolverSource {
         BUILT_IN(0),
         MANUAL(1),
         REGISTRY(2),
-        CLASSPATH(3);
+        PROVIDER(3);
 
         private final int rank;
 
@@ -387,8 +326,8 @@ public class YamlDeserializationContext extends 
StandardConstructor implements C
                             YamlDeserializationContext.class.getName(),
                             YamlDeserializationContext.this);
 
-                    final ConstructNode answer = resolve(n, type.getName());
-                    return answer.construct(newNode);
+                    final ConstructNode constructor = resolve(n, 
type.getName());
+                    return construct(newNode, type.getName(), constructor);
                 },
                 camelContext);
     }
@@ -420,7 +359,7 @@ public class YamlDeserializationContext extends 
StandardConstructor implements C
         }
 
         final String id = ((ScalarNode) key).getValue();
-        final ConstructNode answer = resolve(node, id);
+        final ConstructNode constructor = resolve(node, id);
 
         return CamelContextAware.trySetCamelContext(
                 (Node n) -> {
@@ -429,28 +368,46 @@ public class YamlDeserializationContext extends 
StandardConstructor implements C
                             YamlDeserializationContext.class.getName(),
                             YamlDeserializationContext.this);
 
-                    return answer.construct(
-                            ((MappingNode) 
n).getValue().get(0).getValueNode());
+                    Node valueNode = ((MappingNode) 
n).getValue().get(0).getValueNode();
+                    return construct(valueNode, id, constructor);
                 },
                 camelContext);
     }
 
     public ConstructNode resolve(Node node, String id) {
-        return constructors.computeIfAbsent(id, (String s) -> {
-            ConstructNode answer = null;
-
-            for (ResolverEntry entry : resolvers) {
-                answer = entry.resolver.resolve(id);
-                if (answer != null) {
-                    break;
-                }
-            }
+        return constructors.computeIfAbsent(id, nodeId -> 
resolveConstructor(node, nodeId));
+    }
 
-            if (answer == null) {
-                throw new UnknownNodeIdException(node, id);
+    private ConstructNode resolveConstructor(Node node, String id) {
+        for (ResolverEntry entry : resolvers) {
+            ConstructNode constructor;
+            try {
+                constructor = entry.resolver.resolve(id);
+            } catch (YamlDeserializationException e) {
+                throw e;
+            } catch (RuntimeException e) {
+                throw new YamlDeserializationException(
+                        node,
+                        "Error resolving YAML node id: " + id + " using YAML 
deserializer resolver " + entry.name,
+                        e);
+            }
+            if (constructor != null) {
+                return constructor;
             }
+        }
+
+        throw new UnknownNodeIdException(node, id);
+    }
 
-            return answer;
-        });
+    private Object construct(Node node, String id, ConstructNode constructor) {
+        try {
+            return constructor.construct(node);
+        } catch (DuplicateKeyException | UnknownNodeIdException e) {
+            throw e;
+        } catch (YamlDeserializationException e) {
+            throw e;
+        } catch (RuntimeException e) {
+            throw new YamlDeserializationException(node, "Error constructing 
YAML node id: " + id, e);
+        }
     }
 }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerResolver.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerResolver.java
index a9955e79ea5a..415db8cdeecb 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerResolver.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerResolver.java
@@ -19,6 +19,12 @@ package org.apache.camel.dsl.yaml.common;
 import org.apache.camel.Ordered;
 import org.snakeyaml.engine.v2.api.ConstructNode;
 
+/**
+ * Resolves YAML node ids to SnakeYAML constructors.
+ * <p/>
+ * Optional Camel modules should implement this SPI to contribute custom YAML 
route steps. Runtime integrations that
+ * need to control how resolver implementations are discovered can provide a 
{@link YamlDeserializerResolverProvider}.
+ */
 public interface YamlDeserializerResolver extends Ordered {
 
     /**
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerResolverProvider.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerResolverProvider.java
new file mode 100644
index 000000000000..3b494291ef07
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerResolverProvider.java
@@ -0,0 +1,45 @@
+/*
+ * 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.common;
+
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+
+/**
+ * Strategy for discovering automatically available {@link 
YamlDeserializerResolver} instances.
+ * <p/>
+ * The default implementation discovers resolver class names from {@link 
YamlDeserializerResolver#RESOURCE_PATH}.
+ * Runtimes that perform build-time discovery can register their own provider 
as a Camel context plugin and return the
+ * resolvers without scanning classpath resources at runtime.
+ */
+public interface YamlDeserializerResolverProvider {
+
+    /**
+     * Finds automatically available YAML deserializer resolvers.
+     * <p/>
+     * The {@code camelContext} argument is never {@code null}. 
Implementations must return a non-{@code null} map. Use
+     * {@code Collections.emptyMap()} when no resolvers are available. Map 
keys must be stable, non-{@code null} names
+     * used for diagnostics and deterministic ordering. Map values must be 
non-{@code null}
+     * {@link YamlDeserializerResolver} instances. Discovery failures should 
be reported by throwing a runtime
+     * exception.
+     *
+     * @param  camelContext the Camel context used for resolver discovery and 
instantiation
+     * @return              discovered resolvers keyed by a stable name used 
for diagnostics and deterministic ordering
+     */
+    Map<String, YamlDeserializerResolver> findResolvers(CamelContext 
camelContext);
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/docs/camel-yaml-dsl-validator-maven-plugin.adoc
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/docs/camel-yaml-dsl-validator-maven-plugin.adoc
index f235056945cb..f1cbe3439110 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/docs/camel-yaml-dsl-validator-maven-plugin.adoc
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator-maven-plugin/src/main/docs/camel-yaml-dsl-validator-maven-plugin.adoc
@@ -6,12 +6,12 @@ The Camel YAML DSL Validator Maven Plugin supports the 
following goals
 
 == camel-yaml-dsl-validator:validate
 
-For validating the YAML routes for syntax errors according to the spec.
+For validating YAML routes against the generated YAML DSL JSON Schema.
 
-The validator uses the generated YAML DSL JSON Schema.
 Runtime extensions such as custom `YamlDeserializerResolver` implementations 
are not added to this schema, so the
-validator does not accept module-provided custom YAML step names. For those 
routes, use runtime route loading tests in
-addition to this schema validation.
+`validate` goal does not accept module-provided custom YAML step names. The 
Maven plugin does not run Camel's YAML route
+loader and does not load custom resolvers. For those routes, use runtime route 
loading tests in addition to this schema
+validation.
 
 Then you can run the `validate` goal from the command line or from within your 
Java editor such as IDEA or Eclipse.
 
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorTest.java
index 5605c07f1814..49b2a0966a12 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorTest.java
@@ -57,4 +57,11 @@ public class YamlValidatorTest {
         Assertions.assertEquals(1, report.size());
         
Assertions.assertTrue(report.get(0).getMessage().contains("setCheese"));
     }
+
+    @Test
+    public void testValidateRuntimeCustomStepRejectedBySchema() throws 
Exception {
+        var report = validator.validate(new 
File("src/test/resources/custom-parser-step.yaml"));
+        Assertions.assertFalse(report.isEmpty());
+        Assertions.assertTrue(report.stream().anyMatch(error -> 
error.getMessage().contains("parserStep")));
+    }
 }
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/docs/yaml-dsl.adoc 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/docs/yaml-dsl.adoc
index df4ed3a30ff0..828ea2e286cd 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/docs/yaml-dsl.adoc
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/docs/yaml-dsl.adoc
@@ -130,6 +130,11 @@ com.acme.camel.MyYamlStepResolver
 The resolver returns a SnakeYAML `ConstructNode` for the step names, or model 
class names, it supports.
 The deserializer should create and configure the normal Camel model object 
that the custom YAML step represents:
 
+This SPI only controls YAML deserialization. It does not add route execution 
behavior.
+The returned object must be a Camel route model definition that Camel can 
already execute through existing model reifier
+or `ProcessorFactory` support, or the optional module must provide that 
runtime support using the normal Camel extension
+mechanisms.
+
 [source,java]
 ----
 import org.apache.camel.dsl.yaml.common.YamlDeserializerBase;
@@ -195,16 +200,21 @@ This allows YAML such as:
                 uri: "mock:result"
 ----
 
-Resolvers are discovered when YAML routes are parsed. Camel loads the built-in 
YAML DSL resolvers first, then the
-classpath-discovered resolvers from 
`META-INF/services/org/apache/camel/YamlDeserializerResolver`, and then any
-`YamlDeserializerResolver` beans found in the Camel registry.
-Camel looks for resolver resources through the Camel class resolver, the 
application context classloader, and any
+Resolvers are discovered when YAML routes are parsed. Camel starts with the 
built-in YAML DSL resolvers, then adds the
+resolvers returned by the active `YamlDeserializerResolverProvider` and any 
`YamlDeserializerResolver` beans found in
+the Camel registry.
+When no custom provider is registered as a Camel context plugin, the default 
provider discovers resolver class names from
+`META-INF/services/org/apache/camel/YamlDeserializerResolver`.
+It looks for resolver resources through the Camel class resolver, the 
application context classloader, and any
 classloaders registered with the Camel class resolver.
+Runtime integrations that perform build-time discovery, such as native-image 
runtimes, can register their own
+`YamlDeserializerResolverProvider` as a Camel context plugin to supply the 
automatically available resolvers instead of
+using the default runtime classpath resource scanning provider.
 
 All resolvers are ordered by `YamlDeserializerResolver#getOrder()`, where 
lower order values have higher precedence.
-Built-in YAML DSL resolvers win over classpath-discovered and 
registry-provided resolvers unless the external resolver
+Built-in YAML DSL resolvers win over provider-discovered and registry-provided 
resolvers unless the external resolver
 uses an order lower than `YamlDeserializerResolver.ORDER_DEFAULT`.
-For external resolvers with the same order, registry-provided resolvers are 
tried before classpath-discovered resolvers.
+For external resolvers with the same order, registry-provided resolvers are 
tried before provider-discovered resolvers.
 Within the same source and order, resolvers are ordered by fully qualified 
class name, and registry resolvers use the
 registry bean name as a final tie-breaker.
 If several resolvers can resolve the same step name, the first matching 
resolver wins.
@@ -214,8 +224,14 @@ generated model resolver is also treated as a built-in 
resolver.
 Use an order lower than `ORDER_DEFAULT` only when a custom resolver 
intentionally needs to override any built-in YAML
 name.
 
-Resolver classes are instantiated through Camel's injector. If the resolver 
implements `CamelContextAware`, Camel sets
-the current `CamelContext` before the resolver is used.
+Resolver classes found through the default provider are instantiated through 
Camel's injector.
+If a provider-discovered or registry-provided resolver implements 
`CamelContextAware`, Camel sets the current
+`CamelContext` before the resolver is used.
+Resolver discovery and resolver execution are fail-fast: a broken service 
entry, provider error, or resolver exception
+aborts YAML route loading instead of being silently skipped.
+Resolver implementations should be lightweight and should not rely on Camel 
`Service` lifecycle callbacks from the YAML
+deserialization context. Registry resolvers are owned by the registry, and 
custom providers own the lifecycle of any
+resolver instances they return.
 The same discovery mechanism is used by the YAML routes loader and the Kamelet 
routes loader, so custom route steps can
 be used in regular YAML routes and in Kamelet template step lists.
 
@@ -224,11 +240,17 @@ When a custom step maps to a model definition that 
supports child outputs, the d
 Nested `steps` are only meaningful for model definitions that can contain 
outputs, such as `Block` implementations.
 If the optional module that provides the resolver is not on the classpath, 
Camel does not load that resolver. YAML files
 that use the custom step name then fail with an unknown node error; YAML files 
that do not use the custom step are
-unaffected.
+unaffected. If the service file names a resolver class that cannot be loaded, 
YAML route parsing fails when resolver
+discovery runs.
 
 Custom YAML step names are runtime extensions.
 The generated YAML DSL JSON schemas only describe the built-in DSL and do not 
validate module-provided step names.
-This also applies to IDE validation and schema-based tooling.
+This also applies to IDE validation and schema-based tooling, including the 
YAML DSL validator Maven plugin.
+Runtime route-loading parsers can use custom resolvers that are available on 
their classpath, but schema validation
+remains limited to the generated built-in DSL schema.
+Modules that provide custom YAML steps should include route-loading tests that 
put the resolver service resource on the
+test classpath, load a YAML route using the custom step, and assert that Camel 
created the expected model definition or
+that the route executes as expected. For output-aware custom steps, include at 
least one nested `steps` entry.
 
 YAML routes and resolver providers are trusted route-author and deployment 
inputs under Camel's security model.
 This SPI is not a sandbox for processing untrusted YAML documents.
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/YamlDeserializerResolverDiscoveryTest.groovy
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/YamlDeserializerResolverDiscoveryTest.groovy
index 84058e7620b4..c31d5f6cd166 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/YamlDeserializerResolverDiscoveryTest.groovy
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/YamlDeserializerResolverDiscoveryTest.groovy
@@ -16,9 +16,13 @@
  */
 package org.apache.camel.dsl.yaml
 
+import org.apache.camel.CamelContextAware
+import org.apache.camel.CamelContext
 import org.apache.camel.component.mock.MockEndpoint
 import org.apache.camel.dsl.yaml.common.YamlDeserializerBase
 import org.apache.camel.dsl.yaml.common.YamlDeserializerResolver
+import org.apache.camel.dsl.yaml.common.YamlDeserializerResolverProvider
+import org.apache.camel.dsl.yaml.common.exception.DuplicateKeyException
 import org.apache.camel.dsl.yaml.common.exception.UnknownNodeIdException
 import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException
 import org.apache.camel.dsl.yaml.support.YamlTestSupport
@@ -27,12 +31,13 @@ import org.apache.camel.model.StepDefinition
 import org.apache.camel.model.ToDefinition
 import org.snakeyaml.engine.v2.api.ConstructNode
 import org.snakeyaml.engine.v2.nodes.Node
-import org.snakeyaml.engine.v2.nodes.ScalarNode
 
 import java.nio.charset.StandardCharsets
 import java.nio.file.Files
 import java.nio.file.Path
 
+import static org.apache.camel.dsl.yaml.common.YamlDeserializerSupport.asText
+
 class YamlDeserializerResolverDiscoveryTest extends YamlTestSupport {
 
     def "discover custom route step resolver from classpath"() {
@@ -264,6 +269,168 @@ class YamlDeserializerResolverDiscoveryTest extends 
YamlTestSupport {
             classLoader?.close()
     }
 
+    def "class resolver classloader resolver resource is discovered"() {
+        given:
+            def classLoader = 
resolverResourceClassLoader(ClassResolverStepResolver.name)
+            context.classResolver.addClassLoader(classLoader)
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - classResolverStep: {}
+            '''
+
+        then:
+            with(context.routeDefinitions[0].outputs[0], StepDefinition) {
+                id == 'class-resolver-step'
+            }
+
+        cleanup:
+            classLoader?.close()
+    }
+
+    def "provider discovered resolver receives camel context"() {
+        given:
+            
context.getCamelContextExtension().addContextPlugin(YamlDeserializerResolverProvider.class,
+                    new StaticResolverProvider('contextAwareStepResolver', new 
ContextAwareStepResolver(context)))
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - contextAwareStep: {}
+            '''
+
+        then:
+            with(context.routeDefinitions[0].outputs[0], StepDefinition) {
+                id == 'context-aware-step'
+            }
+    }
+
+    def "context plugin provider contributes custom route step"() {
+        given:
+            
context.getCamelContextExtension().addContextPlugin(YamlDeserializerResolverProvider.class,
+                    new StaticResolverProvider('providerCustomStepResolver',
+                            new FixedStepResolver('providerCustomStep', 
'provider-custom-step',
+                                    YamlDeserializerResolver.ORDER_DEFAULT + 
1)))
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - providerCustomStep: {}
+            '''
+
+        then:
+            with(context.routeDefinitions[0].outputs[0], StepDefinition) {
+                id == 'provider-custom-step'
+            }
+    }
+
+    def "context plugin provider replaces default classpath discovery"() {
+        given:
+            
context.getCamelContextExtension().addContextPlugin(YamlDeserializerResolverProvider.class,
+                    new StaticResolverProvider([:]))
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - customStep: {}
+            '''
+
+        then:
+            def e = thrown(UnknownNodeIdException)
+            e.message.contains('Unknown node id: customStep')
+    }
+
+    def "registry resolver still loads when context plugin provider is 
installed"() {
+        given:
+            
context.getCamelContextExtension().addContextPlugin(YamlDeserializerResolverProvider.class,
+                    new StaticResolverProvider([:]))
+            context.registry.bind('registryStepResolver',
+                    new FixedStepResolver('registryStep', 'registry-step', 
YamlDeserializerResolver.ORDER_DEFAULT))
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - registryStep: {}
+            '''
+
+        then:
+            with(context.routeDefinitions[0].outputs[0], StepDefinition) {
+                id == 'registry-step'
+            }
+    }
+
+    def "provider null resolver map is rejected with diagnostic"() {
+        given:
+            
context.getCamelContextExtension().addContextPlugin(YamlDeserializerResolverProvider.class,
+                    new NullMapResolverProvider())
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - to:
+                          uri: "mock:result"
+            '''
+
+        then:
+            def e = thrown(YamlDeserializationException)
+            e.message.contains('PROVIDER')
+            e.message.contains('null resolver map')
+    }
+
+    def "provider null resolver name is rejected with diagnostic"() {
+        given:
+            
context.getCamelContextExtension().addContextPlugin(YamlDeserializerResolverProvider.class,
+                    new StaticResolverProvider([(null): new 
FixedStepResolver('badStep', 'bad-step',
+                            YamlDeserializerResolver.ORDER_DEFAULT)]))
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - to:
+                          uri: "mock:result"
+            '''
+
+        then:
+            def e = thrown(YamlDeserializationException)
+            e.message.contains('PROVIDER')
+            e.message.contains('null resolver name')
+    }
+
+    def "provider null resolver value is rejected with diagnostic"() {
+        given:
+            
context.getCamelContextExtension().addContextPlugin(YamlDeserializerResolverProvider.class,
+                    new StaticResolverProvider(['nullResolver': null]))
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - to:
+                          uri: "mock:result"
+            '''
+
+        then:
+            def e = thrown(YamlDeserializationException)
+            e.message.contains('PROVIDER')
+            e.message.contains('null resolver for nullResolver')
+    }
+
     def "kamelet route template discovers custom route step"() {
         when:
             loadKamelets '''
@@ -297,6 +464,55 @@ class YamlDeserializerResolverDiscoveryTest extends 
YamlTestSupport {
             }
     }
 
+    def "regular route template discovers custom route step"() {
+        when:
+            loadRoutesNoValidate '''
+                - routeTemplate:
+                    id: "customTemplate"
+                    from:
+                      uri: "direct:{{name}}"
+                      steps:
+                        - customStep:
+                            id: "template-custom-step"
+                            steps:
+                              - to:
+                                  uri: "mock:result"
+            '''
+
+        then:
+            context.routeTemplateDefinitions.size() == 1
+            with(context.routeTemplateDefinitions[0].route.outputs[0], 
StepDefinition) {
+                id == 'template-custom-step'
+                with(outputs[0], ToDefinition) {
+                    endpointUri == 'mock:result'
+                }
+            }
+    }
+
+    def "route configuration discovers custom route step"() {
+        when:
+            loadRoutesNoValidate '''
+                - routeConfiguration:
+                    onCompletion:
+                      - onCompletion:
+                          steps:
+                            - customStep:
+                                id: "route-configuration-custom-step"
+                                steps:
+                                  - to:
+                                      uri: "mock:on-completion"
+            '''
+
+        then:
+            context.getRouteConfigurationDefinitions().size() == 1
+            
with(context.getRouteConfigurationDefinitions().get(0).getOnCompletions().get(0).outputs[0],
 StepDefinition) {
+                id == 'route-configuration-custom-step'
+                with(outputs[0], ToDefinition) {
+                    endpointUri == 'mock:on-completion'
+                }
+            }
+    }
+
     def "unknown custom step reports node id"() {
         when:
             loadRoutesNoValidate '''
@@ -311,6 +527,107 @@ class YamlDeserializerResolverDiscoveryTest extends 
YamlTestSupport {
             e.message.contains('Unknown node id: missingOptionalStep')
     }
 
+    def "custom route step with duplicate sibling keys reports duplicate 
key"() {
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - customStep: {}
+                        to:
+                          uri: "mock:result"
+            '''
+
+        then:
+            thrown(DuplicateKeyException)
+    }
+
+    def "malformed custom route step scalar shape reports custom node id"() {
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - customStep: "not-a-map"
+            '''
+
+        then:
+            def e = thrown(YamlDeserializationException)
+            e.message.contains('Error constructing YAML node id: customStep')
+            e.cause instanceof UnsupportedOperationException
+    }
+
+    def "malformed custom route step id shape reports custom node id"() {
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - customStep:
+                          id:
+                            - bad
+            '''
+
+        then:
+            def e = thrown(YamlDeserializationException)
+            e.message.contains('Error constructing YAML node id: customStep')
+    }
+
+    def "malformed custom route step steps shape reports expected sequence"() {
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - customStep:
+                          steps: "not-a-list"
+            '''
+
+        then:
+            def e = thrown(YamlDeserializationException)
+            e.message.contains('Error constructing YAML node id: customStep')
+            e.cause.message.contains('expected array')
+    }
+
+    def "resolver failure reports resolver and node id"() {
+        given:
+            
context.getCamelContextExtension().addContextPlugin(YamlDeserializerResolverProvider.class,
+                    new StaticResolverProvider('throwingStepResolver', new 
ThrowingStepResolver()))
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - failingStep: {}
+            '''
+
+        then:
+            def e = thrown(YamlDeserializationException)
+            e.message.contains('Error resolving YAML node id: failingStep')
+            e.message.contains('throwingStepResolver')
+            e.cause instanceof IllegalStateException
+    }
+
+    def "custom step constructor failure reports custom node id"() {
+        given:
+            
context.getCamelContextExtension().addContextPlugin(YamlDeserializerResolverProvider.class,
+                    new 
StaticResolverProvider('failingConstructorStepResolver', new 
FailingConstructorStepResolver()))
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - constructFailureStep: {}
+            '''
+
+        then:
+            def e = thrown(YamlDeserializationException)
+            e.message.contains('Error constructing YAML node id: 
constructFailureStep')
+            e.cause instanceof IllegalStateException
+    }
+
     def "broken resolver service entry reports provider class"() {
         given:
             def classLoader = 
resolverResourceClassLoader('com.acme.camel.MissingYamlStepResolver')
@@ -334,6 +651,30 @@ class YamlDeserializerResolverDiscoveryTest extends 
YamlTestSupport {
             classLoader?.close()
     }
 
+    def "unreadable resolver resource reports sanitized resource location"() {
+        given:
+            context.applicationContextClassLoader = 
unreadableResolverResourceClassLoader()
+
+        when:
+            loadRoutesNoValidate '''
+                - from:
+                    uri: "direct:start"
+                    steps:
+                      - to:
+                          uri: "mock:result"
+            '''
+
+        then:
+            def e = thrown(YamlDeserializationException)
+            e.message.contains(YamlDeserializerResolver.RESOURCE_PATH)
+            e.message.contains('resolver resource')
+            !e.message.contains('user:secret')
+            !e.message.contains('internal.example.local')
+            !e.message.contains('very-secret-path')
+            !e.message.contains('token=')
+            !e.message.contains('fragment')
+    }
+
     private static URLClassLoader resolverResourceClassLoader(String 
resolverLine) {
         Path root = Files.createTempDirectory('yaml-resolver-provider')
         Path serviceDirectory = 
root.resolve('META-INF/services/org/apache/camel')
@@ -345,6 +686,36 @@ class YamlDeserializerResolverDiscoveryTest extends 
YamlTestSupport {
         return new URLClassLoader([root.toUri().toURL()] as URL[], 
Thread.currentThread().contextClassLoader)
     }
 
+    private static ClassLoader unreadableResolverResourceClassLoader() {
+        URL resolverResource = new URL(null,
+                
'resolver://user:[email protected]/very-secret-path?token=abc#fragment',
+                new URLStreamHandler() {
+                    @Override
+                    protected URLConnection openConnection(URL url) {
+                        return new URLConnection(url) {
+                            @Override
+                            void connect() {
+                            }
+
+                            @Override
+                            InputStream getInputStream() {
+                                throw new IOException('cannot read resolver 
resource')
+                            }
+                        }
+                    }
+                })
+
+        return new ClassLoader(Thread.currentThread().contextClassLoader) {
+            @Override
+            Enumeration<URL> getResources(String name) throws IOException {
+                if (name == YamlDeserializerResolver.RESOURCE_PATH) {
+                    return Collections.enumeration([resolverResource])
+                }
+                return super.getResources(name)
+            }
+        }
+    }
+
     static class CustomStepResolver implements YamlDeserializerResolver {
         @Override
         int getOrder() {
@@ -394,7 +765,7 @@ class YamlDeserializerResolverDiscoveryTest extends 
YamlTestSupport {
         protected boolean setProperty(StepDefinition target, String 
propertyKey, String propertyName, Node value) {
             switch (propertyKey) {
                 case 'id':
-                    target.setId(((ScalarNode)value).value)
+                    target.setId(asText(value))
                     break
                 case 'steps':
                     setSteps(target, value)
@@ -493,4 +864,89 @@ class YamlDeserializerResolverDiscoveryTest extends 
YamlTestSupport {
             super('applicationStep', 'application-context-step', 
YamlDeserializerResolver.ORDER_DEFAULT + 1)
         }
     }
+
+    static class ClassResolverStepResolver extends FixedStepResolver {
+        ClassResolverStepResolver() {
+            super('classResolverStep', 'class-resolver-step', 
YamlDeserializerResolver.ORDER_DEFAULT + 1)
+        }
+    }
+
+    static class ContextAwareStepResolver extends FixedStepResolver implements 
CamelContextAware {
+        private final CamelContext expectedCamelContext
+        private CamelContext camelContext
+
+        ContextAwareStepResolver(CamelContext expectedCamelContext) {
+            super('contextAwareStep', 'context-aware-step', 
YamlDeserializerResolver.ORDER_DEFAULT + 1)
+            this.expectedCamelContext = expectedCamelContext
+        }
+
+        @Override
+        CamelContext getCamelContext() {
+            return camelContext
+        }
+
+        @Override
+        void setCamelContext(CamelContext camelContext) {
+            this.camelContext = camelContext
+        }
+
+        @Override
+        ConstructNode resolve(String id) {
+            if (camelContext.is(expectedCamelContext)) {
+                return super.resolve(id)
+            }
+            return null
+        }
+    }
+
+    static class ThrowingStepResolver implements YamlDeserializerResolver {
+        @Override
+        ConstructNode resolve(String id) {
+            if (id == 'failingStep') {
+                throw new IllegalStateException('resolver boom')
+            }
+            return null
+        }
+    }
+
+    static class FailingConstructorStepResolver implements 
YamlDeserializerResolver {
+        @Override
+        ConstructNode resolve(String id) {
+            if (id == 'constructFailureStep') {
+                return new FailingConstructor()
+            }
+            return null
+        }
+    }
+
+    static class FailingConstructor implements ConstructNode {
+        @Override
+        Object construct(Node node) {
+            throw new IllegalStateException('constructor boom')
+        }
+    }
+
+    static class NullMapResolverProvider implements 
YamlDeserializerResolverProvider {
+        @Override
+        Map<String, YamlDeserializerResolver> findResolvers(CamelContext 
camelContext) {
+            return null
+        }
+    }
+
+    static class StaticResolverProvider implements 
YamlDeserializerResolverProvider {
+        private final Map<String, YamlDeserializerResolver> resolvers
+
+        StaticResolverProvider(String name, YamlDeserializerResolver resolver) 
{
+            this([(name): resolver])
+        }
+
+        StaticResolverProvider(Map<String, YamlDeserializerResolver> 
resolvers) {
+            this.resolvers = resolvers
+        }
+
+        @Override
+        Map<String, YamlDeserializerResolver> findResolvers(CamelContext 
camelContext) {
+            return resolvers
+        }
+    }
 }

Reply via email to