This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-24709
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 174946ab38d872493ad04c75f572573c3e697a33
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 15 10:15:27 2026 +0200

    CAMEL-24709: camel-yaml-dsl - class not found hint for beans uses the bean 
metadata on the classpath
    
    The YAML beans hint from CAMEL-24702 only knew the aggregation strategies in
    org.apache.camel.processor.aggregate. It now reads the bean metadata the 
build
    generates into every JAR 
(META-INF/services/org/apache/camel/bean.properties and
    bean/<Name>.json), so any built-in bean written with the wrong package or no
    package gets "did you mean <javaType> (<interface>)? write: type: 
<javaType>".
    
    The same metadata backs the reifiers: a #class: aggregation strategy, 
repository
    or other typed lookup whose class does not exist now fails with 
NoSuchBeanException
    that names the built-in bean that was likely meant, or lists the built-in 
beans of
    the expected interface, instead of a bare "Cannot find AggregationStrategy 
in
    Registry" or "No bean could be found ... of type".
    
    - camel-support: PojoBeanHelper scans bean.properties / bean/*.json on the
      error path (findAll, findByName, beansOfInterface, classNotFoundHint)
    - camel-api: NoSuchBeanException(name, type, hint) constructor
    - camel-core-reifier: AbstractReifier.lookupByName(name, expectedType) and
      mandatoryLookup add the hint; the four aggregation strategy lookups use it
    - camel-yaml-dsl: BeansDeserializer.classNotFoundHint is data driven
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../java/org/apache/camel/NoSuchBeanException.java |  12 +
 .../org/apache/camel/reifier/AbstractReifier.java  |  22 +-
 .../org/apache/camel/reifier/MulticastReifier.java |   2 +-
 .../org/apache/camel/reifier/ProcessorReifier.java |   2 +-
 .../apache/camel/reifier/RecipientListReifier.java |   2 +-
 .../org/apache/camel/reifier/SplitReifier.java     |   2 +-
 .../AggregationStrategyClassNotFoundHintTest.java  | 113 ++++++++++
 .../apache/camel/support/PojoBeanHelperTest.java   | 131 +++++++++++
 .../org/apache/camel/support/EndpointHelper.java   |   4 +-
 .../org/apache/camel/support/PojoBeanHelper.java   | 243 +++++++++++++++++++++
 .../dsl/yaml/deserializers/BeansDeserializer.java  |  39 ++--
 .../org/apache/camel/dsl/yaml/AggregateTest.groovy |  45 ++++
 .../org/apache/camel/dsl/yaml/BeansTest.groovy     |  56 +++++
 13 files changed, 645 insertions(+), 28 deletions(-)

diff --git 
a/core/camel-api/src/main/java/org/apache/camel/NoSuchBeanException.java 
b/core/camel-api/src/main/java/org/apache/camel/NoSuchBeanException.java
index f02c11069283..e64e2c4acf1a 100644
--- a/core/camel-api/src/main/java/org/apache/camel/NoSuchBeanException.java
+++ b/core/camel-api/src/main/java/org/apache/camel/NoSuchBeanException.java
@@ -78,6 +78,18 @@ public class NoSuchBeanException extends 
RuntimeCamelException {
         this.name = name;
     }
 
+    /**
+     * @param name the bean name that could not be found
+     * @param type the required bean type
+     * @param hint what to do about it, appended to the message (such as the 
built-in bean that was likely meant)
+     * @since      4.23
+     */
+    public NoSuchBeanException(String name, String type, String hint) {
+        super("No bean could be found in the registry for: " + 
Objects.requireNonNull(name, "name") + " of type: "
+              + Objects.requireNonNull(type, "type") + (hint != null ? hint : 
""));
+        this.name = name;
+    }
+
     /**
      * @param name    the bean name that could not be found
      * @param message the detail message
diff --git 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/AbstractReifier.java
 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/AbstractReifier.java
index f7e32a4740ea..ae4a62f2939b 100644
--- 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/AbstractReifier.java
+++ 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/AbstractReifier.java
@@ -38,6 +38,7 @@ import org.apache.camel.reifier.language.ExpressionReifier;
 import org.apache.camel.spi.BeanRepository;
 import org.apache.camel.support.CamelContextHelper;
 import org.apache.camel.support.EndpointHelper;
+import org.apache.camel.support.PojoBeanHelper;
 import org.apache.camel.util.ObjectHelper;
 
 public abstract class AbstractReifier implements BeanRepository {
@@ -178,7 +179,8 @@ public abstract class AbstractReifier implements 
BeanRepository {
 
         Object obj = lookupByNameAndType(name, type);
         if (obj == null) {
-            throw new NoSuchBeanException(name, type.getName());
+            // a #class: whose class was not found: say which built-in bean 
was likely meant
+            throw new NoSuchBeanException(name, type.getName(), 
PojoBeanHelper.classNotFoundHint(camelContext, name, type));
         }
         return type.cast(obj);
     }
@@ -201,6 +203,24 @@ public abstract class AbstractReifier implements 
BeanRepository {
         return answer;
     }
 
+    /**
+     * Looks up a bean by name as {@link #lookupByName(String)} does; the bean 
may be of any type (such as a POJO that
+     * is adapted afterwards). A <tt>#class:</tt> whose class does not exist 
fails here, with the built-in bean of the
+     * expected type that was likely meant, instead of answering <tt>null</tt>.
+     *
+     * @param name         the bean name or <tt>#class:</tt> reference
+     * @param expectedType the type the bean is expected to be, for the error 
message only
+     */
+    public Object lookupByName(String name, Class<?> expectedType) {
+        Object answer = lookupByName(name);
+        if (answer == null && name != null && 
parseString(name).startsWith("#class:")) {
+            name = parseString(name);
+            throw new NoSuchBeanException(
+                    name, expectedType.getName(), 
PojoBeanHelper.classNotFoundHint(camelContext, name, expectedType));
+        }
+        return answer;
+    }
+
     public <T> T lookupByNameAndType(String name, Class<T> type) {
         if (name == null) {
             return null;
diff --git 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/MulticastReifier.java
 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/MulticastReifier.java
index a50c7c1791eb..ef1df16ddcec 100644
--- 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/MulticastReifier.java
+++ 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/MulticastReifier.java
@@ -90,7 +90,7 @@ public class MulticastReifier extends 
ProcessorReifier<MulticastDefinition> {
         AggregationStrategy strategy = definition.getAggregationStrategyBean();
         String ref = parseString(definition.getAggregationStrategy());
         if (strategy == null && ref != null) {
-            Object aggStrategy = lookupByName(ref);
+            Object aggStrategy = lookupByName(ref, AggregationStrategy.class);
             if (aggStrategy == null) {
                 aggStrategy = lookupByNameAndType(ref, 
AggregationStrategy.class);
             }
diff --git 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ProcessorReifier.java
 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ProcessorReifier.java
index 2d964fa98a2d..30e93a906380 100644
--- 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ProcessorReifier.java
+++ 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/ProcessorReifier.java
@@ -934,7 +934,7 @@ public abstract class ProcessorReifier<T extends 
ProcessorDefinition<?>> extends
     public AggregationStrategy 
getConfiguredAggregationStrategy(AggregationStrategyAwareDefinition<?> 
definition) {
         AggregationStrategy strategy = definition.getAggregationStrategyBean();
         if (strategy == null && definition.getAggregationStrategyRef() != 
null) {
-            Object aggStrategy = 
lookupByName(definition.getAggregationStrategyRef());
+            Object aggStrategy = 
lookupByName(definition.getAggregationStrategyRef(), AggregationStrategy.class);
             if (aggStrategy == null) {
                 aggStrategy = 
lookupByNameAndType(definition.getAggregationStrategyRef(), 
AggregationStrategy.class);
             }
diff --git 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/RecipientListReifier.java
 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/RecipientListReifier.java
index 08febd7be9cd..a8b015be97f1 100644
--- 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/RecipientListReifier.java
+++ 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/RecipientListReifier.java
@@ -117,7 +117,7 @@ public class RecipientListReifier extends 
ProcessorReifier<RecipientListDefiniti
         AggregationStrategy strategy = definition.getAggregationStrategyBean();
         String ref = parseString(definition.getAggregationStrategy());
         if (strategy == null && ref != null) {
-            Object aggStrategy = lookupByName(ref);
+            Object aggStrategy = lookupByName(ref, AggregationStrategy.class);
             if (aggStrategy == null) {
                 aggStrategy = lookupByNameAndType(ref, 
AggregationStrategy.class);
             }
diff --git 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SplitReifier.java
 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SplitReifier.java
index 7c6e2c3db63c..a9abe990aebd 100644
--- 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SplitReifier.java
+++ 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SplitReifier.java
@@ -150,7 +150,7 @@ public class SplitReifier extends 
ExpressionReifier<SplitDefinition> {
     private AggregationStrategy createAggregationStrategy() {
         AggregationStrategy strategy = definition.getAggregationStrategyBean();
         if (strategy == null && definition.getAggregationStrategy() != null) {
-            Object aggStrategy = 
lookupByName(definition.getAggregationStrategy());
+            Object aggStrategy = 
lookupByName(definition.getAggregationStrategy(), AggregationStrategy.class);
             if (aggStrategy == null) {
                 aggStrategy = 
lookupByNameAndType(definition.getAggregationStrategy(), 
AggregationStrategy.class);
             }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregationStrategyClassNotFoundHintTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregationStrategyClassNotFoundHintTest.java
new file mode 100644
index 000000000000..721b98c7282d
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregationStrategyClassNotFoundHintTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.processor.aggregator;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.NoSuchBeanException;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A #class: aggregation strategy whose class does not exist fails with the 
built-in strategy that was likely meant
+ * (from the bean metadata on the classpath), or with the built-in strategies 
to pick from.
+ */
+public class AggregationStrategyClassNotFoundHintTest extends 
ContextTestSupport {
+
+    @Override
+    public boolean isUseRouteBuilder() {
+        return false;
+    }
+
+    @Test
+    public void testWrongPackageSaysDidYouMean() {
+        String msg = 
startWithStrategy("#class:com.foo.UseLatestAggregationStrategy");
+
+        assertTrue(msg.contains("com.foo.UseLatestAggregationStrategy"), msg);
+        assertTrue(msg.contains("did you mean 
org.apache.camel.processor.aggregate.UseLatestAggregationStrategy"
+                                + " (org.apache.camel.AggregationStrategy)?"),
+                msg);
+    }
+
+    @Test
+    public void testNoPackageSaysDidYouMean() {
+        String msg = 
startWithStrategy("#class:GroupedBodyAggregationStrategy");
+
+        assertTrue(msg.contains("did you mean 
org.apache.camel.processor.aggregate.GroupedBodyAggregationStrategy"
+                                + " (org.apache.camel.AggregationStrategy)?"),
+                msg);
+    }
+
+    @Test
+    public void testUnknownClassListsBuiltInStrategies() {
+        String msg = startWithStrategy("#class:com.foo.MyStrategy");
+
+        assertFalse(msg.contains("did you mean"), msg);
+        assertTrue(msg.contains("check the package name; a class from another 
library needs its dependency added"), msg);
+        assertTrue(msg.contains("the built-in AggregationStrategy beans are"), 
msg);
+        assertTrue(msg.contains("AggregationStrategy 
(org.apache.camel.processor.aggregate."), msg);
+        // more than 6 built-in strategies on the classpath, so the rest are 
counted
+        assertTrue(msg.contains(" more)"), msg);
+    }
+
+    @Test
+    public void testSplitStrategyAlsoGetsTheHint() {
+        Exception e = assertThrows(Exception.class, () -> {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    
from("direct:start").split(body()).aggregationStrategy("#class:com.foo.StringAggregationStrategy")
+                            .to("mock:result");
+                }
+            });
+            context.start();
+        });
+        String msg = messages(e);
+
+        assertTrue(msg.contains("did you mean 
org.apache.camel.processor.aggregate.StringAggregationStrategy"), msg);
+    }
+
+    private String startWithStrategy(String ref) {
+        Exception e = assertThrows(Exception.class, () -> {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    
from("direct:start").aggregate(header("id")).aggregationStrategy(ref).completionSize(1)
+                            .to("mock:result");
+                }
+            });
+            context.start();
+        });
+        String msg = messages(e);
+        assertTrue(msg.contains("No bean could be found in the registry for: " 
+ ref), msg);
+        return msg;
+    }
+
+    private static String messages(Throwable e) {
+        StringBuilder sb = new StringBuilder();
+        boolean noSuchBean = false;
+        for (Throwable t = e; t != null; t = t.getCause()) {
+            noSuchBean |= t instanceof NoSuchBeanException;
+            sb.append(t.getMessage()).append('\n');
+        }
+        assertTrue(noSuchBean, sb.toString());
+        return sb.toString();
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/PojoBeanHelperTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/PojoBeanHelperTest.java
new file mode 100644
index 000000000000..e19ea5a00980
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/PojoBeanHelperTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.support;
+
+import java.util.List;
+
+import org.apache.camel.AggregationStrategy;
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.AggregationRepository;
+import org.apache.camel.spi.IdempotentRepository;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PojoBeanHelperTest {
+
+    private final CamelContext context = new DefaultCamelContext();
+
+    @Test
+    public void testFindAllReadsEveryJar() {
+        List<PojoBeanHelper.PojoBean> beans = PojoBeanHelper.findAll(context);
+
+        // camel-core-processor and camel-support both ship beans
+        assertTrue(beans.stream().anyMatch(b -> 
b.name().equals("UseLatestAggregationStrategy")), beans.toString());
+        assertTrue(beans.stream().anyMatch(b -> 
b.name().equals("MemoryIdempotentRepository")), beans.toString());
+        assertTrue(beans.stream().anyMatch(b -> 
b.name().equals("MemoryAggregationRepository")), beans.toString());
+        // no duplicates when the same jar is visible through more than one 
class loader
+        assertEquals(beans.size(), 
beans.stream().map(PojoBeanHelper.PojoBean::javaType).distinct().count());
+    }
+
+    @Test
+    public void testFindByName() {
+        PojoBeanHelper.PojoBean bean = PojoBeanHelper.findByName(context, 
"com.foo.UseLatestAggregationStrategy");
+        assertNotNull(bean);
+        assertEquals("UseLatestAggregationStrategy", bean.name());
+        
assertEquals("org.apache.camel.processor.aggregate.UseLatestAggregationStrategy",
 bean.javaType());
+        assertEquals("org.apache.camel.AggregationStrategy", 
bean.interfaceType());
+        assertEquals("org.apache.camel", bean.groupId());
+        assertEquals("camel-core-processor", bean.artifactId());
+
+        // simple name, case-insensitive
+        assertNotNull(PojoBeanHelper.findByName(context, 
"memoryAggregationRepository"));
+        assertNull(PojoBeanHelper.findByName(context, "com.foo.MyBean"));
+        assertNull(PojoBeanHelper.findByName(context, null));
+    }
+
+    @Test
+    public void testBeansOfInterface() {
+        List<PojoBeanHelper.PojoBean> beans = 
PojoBeanHelper.beansOfInterface(context, IdempotentRepository.class.getName());
+        assertTrue(beans.stream().anyMatch(b -> 
b.name().equals("MemoryIdempotentRepository")), beans.toString());
+        assertTrue(beans.stream().anyMatch(b -> 
b.name().equals("FileIdempotentRepository")), beans.toString());
+        assertTrue(beans.stream().allMatch(b -> 
b.interfaceType().equals(IdempotentRepository.class.getName())));
+
+        // by simple name too
+        assertEquals(beans.size(), PojoBeanHelper.beansOfInterface(context, 
"IdempotentRepository").size());
+        assertTrue(PojoBeanHelper.beansOfInterface(context, 
"com.foo.Unknown").isEmpty());
+    }
+
+    @Test
+    public void testClassNotFoundHintWrongPackage() {
+        String hint = PojoBeanHelper.classNotFoundHint(context, 
"com.foo.UseLatestAggregationStrategy",
+                AggregationStrategy.class);
+        assertEquals(" (did you mean 
org.apache.camel.processor.aggregate.UseLatestAggregationStrategy"
+                     + " (org.apache.camel.AggregationStrategy)?)",
+                hint);
+        // the expected type does not matter when the name is known
+        assertEquals(hint, PojoBeanHelper.classNotFoundHint(context, 
"com.foo.UseLatestAggregationStrategy", null));
+        assertEquals(hint, PojoBeanHelper.classNotFoundHint(context, 
"UseLatestAggregationStrategy", Object.class));
+        // a #class: reference, with or without constructor parameters or a 
factory method
+        assertEquals(hint, PojoBeanHelper.classNotFoundHint(context, 
"#class:com.foo.UseLatestAggregationStrategy", null));
+        assertEquals(hint,
+                PojoBeanHelper.classNotFoundHint(context, 
"#class:com.foo.UseLatestAggregationStrategy('a', 1)", null));
+        assertEquals(hint,
+                PojoBeanHelper.classNotFoundHint(context, 
"#class:com.foo.UseLatestAggregationStrategy#create", null));
+        // other references are bean names, not classes
+        assertEquals("", PojoBeanHelper.classNotFoundHint(context, 
"#bean:UseLatestAggregationStrategy", null));
+        assertEquals("", PojoBeanHelper.classNotFoundHint(context, 
"#UseLatestAggregationStrategy", null));
+    }
+
+    @Test
+    public void testClassNotFoundHintUnknownClass() {
+        assertEquals(" (check the package name; a class from another library 
needs its dependency added)",
+                PojoBeanHelper.classNotFoundHint(context, "com.foo.MyBean", 
null));
+        assertEquals(" (check the package name; a class from another library 
needs its dependency added)",
+                PojoBeanHelper.classNotFoundHint(context, "com.foo.MyBean", 
Object.class));
+        assertEquals(" (check the package name; a class from another library 
needs its dependency added)",
+                PojoBeanHelper.classNotFoundHint(context, "com.foo.MyBean", 
Runnable.class));
+        assertEquals("", PojoBeanHelper.classNotFoundHint(context, null, 
AggregationStrategy.class));
+    }
+
+    @Test
+    public void testClassNotFoundHintListsBuiltInBeansOfTheExpectedType() {
+        String hint = PojoBeanHelper.classNotFoundHint(context, 
"com.foo.MyRepo", AggregationRepository.class);
+        assertTrue(hint.startsWith(" (check the package name; a class from 
another library needs its dependency added;"
+                                   + " the built-in AggregationRepository 
beans are "),
+                hint);
+        assertTrue(hint.contains(
+                "MemoryAggregationRepository 
(org.apache.camel.processor.aggregate.MemoryAggregationRepository)"), hint);
+        assertTrue(hint.endsWith(")"), hint);
+        assertFalse(hint.contains("IdempotentRepository"), hint);
+    }
+
+    @Test
+    public void testMissingClassName() {
+        assertEquals("com.foo.Bar", PojoBeanHelper.missingClassName(new 
ClassNotFoundException("com.foo.Bar")));
+        assertEquals("com.foo.Bar", PojoBeanHelper.missingClassName(
+                new RuntimeException("wrapped", new 
ClassNotFoundException("com.foo.Bar"))));
+        assertEquals("com.foo.Bar", PojoBeanHelper.missingClassName(new 
NoClassDefFoundError("com/foo/Bar")));
+        assertNull(PojoBeanHelper.missingClassName(new 
IllegalStateException("nope")));
+        assertNull(PojoBeanHelper.missingClassName(null));
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/EndpointHelper.java 
b/core/camel-support/src/main/java/org/apache/camel/support/EndpointHelper.java
index 05a3d91f3e2d..21228c57be3d 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/EndpointHelper.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/EndpointHelper.java
@@ -385,7 +385,9 @@ public final class EndpointHelper {
 
         if (mandatory && answer == null) {
             if (type != null) {
-                throw new NoSuchBeanException(value, type.getTypeName());
+                // a #class: whose class was not found: say which built-in 
bean was likely meant
+                throw new NoSuchBeanException(
+                        value, type.getTypeName(), 
PojoBeanHelper.classNotFoundHint(context, value, type));
             } else {
                 throw new NoSuchBeanException(value);
             }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/PojoBeanHelper.java 
b/core/camel-support/src/main/java/org/apache/camel/support/PojoBeanHelper.java
new file mode 100644
index 000000000000..b37784673684
--- /dev/null
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/PojoBeanHelper.java
@@ -0,0 +1,243 @@
+/*
+ * 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.support;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Looks up the beans that Camel ships out of the box (aggregation strategies, 
idempotent and aggregation repositories,
+ * header filter strategies, ...) from the metadata the build tools generate 
into every JAR that has such beans:
+ * <tt>META-INF/services/org/apache/camel/bean.properties</tt> lists the bean 
names, and
+ * <tt>META-INF/services/org/apache/camel/bean/&lt;Name&gt;.json</tt> has the 
class, the interface and the artifact.
+ * <p/>
+ * The metadata is read when a class cannot be found, so the error can say 
which built-in bean was likely meant. The
+ * classpath is scanned on each call; this is an error-path helper and not 
meant for hot paths.
+ */
+public final class PojoBeanHelper {
+
+    /** Where the build tools generate the bean names of a JAR (one file per 
JAR). */
+    public static final String BEAN_PROPERTIES = 
"META-INF/services/org/apache/camel/bean.properties";
+
+    /** Where the build tools generate the metadata of a bean (one file per 
bean). */
+    public static final String BEAN_JSON_PATH = 
"META-INF/services/org/apache/camel/bean/";
+
+    /** How many built-in beans of an interface a hint lists before saying 
"and N more". */
+    private static final int MAX_LISTED = 6;
+
+    /**
+     * A bean Camel ships out of the box, as described by its generated 
metadata.
+     *
+     * @param name          the bean name (the simple class name)
+     * @param javaType      the fully qualified class name
+     * @param interfaceType the fully qualified name of the interface the bean 
implements (may be null)
+     * @param groupId       the Maven groupId of the artifact that ships the 
bean
+     * @param artifactId    the Maven artifactId of the artifact that ships 
the bean
+     */
+    public record PojoBean(String name, String javaType, String interfaceType, 
String groupId, String artifactId) {
+    }
+
+    private PojoBeanHelper() {
+    }
+
+    /**
+     * All the built-in beans whose metadata is on the classpath.
+     */
+    public static List<PojoBean> findAll(CamelContext camelContext) {
+        List<PojoBean> answer = new ArrayList<>();
+        for (URL url : findBeanProperties(camelContext)) {
+            Properties props = new Properties();
+            try (InputStream is = url.openStream()) {
+                props.load(is);
+            } catch (IOException e) {
+                continue;
+            }
+            String names = props.getProperty("bean", "");
+            for (String name : names.split("\\s+")) {
+                if (!name.isBlank()) {
+                    PojoBean bean = loadBean(camelContext, name.trim(), props);
+                    if (bean != null) {
+                        answer.add(bean);
+                    }
+                }
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * The built-in bean with the given name, matched case-insensitively by 
the simple class name or by the fully
+     * qualified class name, or <tt>null</tt> if there is none.
+     */
+    public static PojoBean findByName(CamelContext camelContext, String name) {
+        if (name == null || name.isBlank()) {
+            return null;
+        }
+        String simple = name.substring(name.lastIndexOf('.') + 1);
+        for (PojoBean bean : findAll(camelContext)) {
+            if (bean.name().equalsIgnoreCase(simple) || 
bean.javaType().equalsIgnoreCase(name)) {
+                return bean;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * The built-in beans of an interface, given by its fully qualified or 
simple name.
+     */
+    public static List<PojoBean> beansOfInterface(CamelContext camelContext, 
String interfaceName) {
+        List<PojoBean> answer = new ArrayList<>();
+        if (interfaceName == null || interfaceName.isBlank()) {
+            return answer;
+        }
+        String simple = interfaceName.substring(interfaceName.lastIndexOf('.') 
+ 1);
+        for (PojoBean bean : findAll(camelContext)) {
+            String iface = bean.interfaceType();
+            if (iface != null && (iface.equals(interfaceName) || 
iface.endsWith("." + simple))) {
+                answer.add(bean);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * What to do about a class that was not found, from the built-in bean 
metadata: the built-in bean with that simple
+     * name (a wrong package), or the built-in beans of the interface the 
class was expected to implement. The metadata
+     * is in the same JAR as the bean, so a built-in bean whose JAR is missing 
gets the generic hint.
+     *
+     * @param  camelContext the camel context
+     * @param  className    the class that was not found (fully qualified or 
simple name), or a <tt>#class:</tt>
+     *                      reference to it; anything else (a bean name) gives 
no hint
+     * @param  expectedType the interface the class was expected to implement, 
or <tt>null</tt> (or Object) if unknown
+     * @return              the hint, in parentheses with a leading space, or 
an empty string if there is nothing to say
+     */
+    public static String classNotFoundHint(CamelContext camelContext, String 
className, Class<?> expectedType) {
+        if (className == null || className.isBlank()) {
+            return "";
+        }
+        if (className.startsWith("#")) {
+            if (!className.startsWith("#class:")) {
+                return "";
+            }
+            // strip the reference prefix and any factory method or 
constructor parameters
+            className = className.substring(7);
+            className = StringHelper.before(className, "(", className);
+            className = StringHelper.before(className, "#", className);
+        }
+        PojoBean bean = findByName(camelContext, className);
+        if (bean != null && !bean.javaType().equals(className)) {
+            return " (did you mean " + bean.javaType()
+                   + (bean.interfaceType() != null ? " (" + 
bean.interfaceType() + ")" : "") + "?)";
+        }
+        String hint = "check the package name; a class from another library 
needs its dependency added";
+        if (expectedType != null && expectedType != Object.class) {
+            List<PojoBean> beans = beansOfInterface(camelContext, 
expectedType.getName());
+            if (!beans.isEmpty()) {
+                StringBuilder sb = new StringBuilder();
+                for (int i = 0; i < beans.size() && i < MAX_LISTED; i++) {
+                    if (i > 0) {
+                        sb.append(", ");
+                    }
+                    sb.append(beans.get(i).name()).append(" 
(").append(beans.get(i).javaType()).append(")");
+                }
+                if (beans.size() > MAX_LISTED) {
+                    sb.append(" and ").append(beans.size() - 
MAX_LISTED).append(" more");
+                }
+                hint += "; the built-in " + expectedType.getSimpleName() + " 
beans are " + sb;
+            }
+        }
+        return " (" + hint + ")";
+    }
+
+    /**
+     * The class named by a ClassNotFoundException or NoClassDefFoundError in 
the cause chain, in dotted form, or
+     * <tt>null</tt> if the failure is not a missing class.
+     */
+    public static String missingClassName(Throwable e) {
+        for (Throwable t = e; t != null; t = t.getCause()) {
+            if (t instanceof ClassNotFoundException || t instanceof 
NoClassDefFoundError) {
+                // NoClassDefFoundError names the class in internal form 
(java/lang/Foo)
+                return t.getMessage() != null ? 
t.getMessage().trim().replace('/', '.') : null;
+            }
+        }
+        return null;
+    }
+
+    private static PojoBean loadBean(CamelContext camelContext, String name, 
Properties props) {
+        try (InputStream is = 
camelContext.getClassResolver().loadResourceAsStream(BEAN_JSON_PATH + name + 
".json")) {
+            if (is == null) {
+                return null;
+            }
+            JsonObject root = (JsonObject) 
Jsoner.deserialize(IOHelper.loadText(is));
+            JsonObject bean = root.getMap("bean");
+            if (bean == null || bean.getString("javaType") == null) {
+                return null;
+            }
+            return new PojoBean(
+                    bean.getStringOrDefault("name", name), 
bean.getString("javaType"), bean.getString("interfaceType"),
+                    bean.getStringOrDefault("groupId", 
props.getProperty("groupId")),
+                    bean.getStringOrDefault("artifactId", 
props.getProperty("artifactId")));
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static List<URL> findBeanProperties(CamelContext camelContext) {
+        // dedupe by URI as URL equals/hashCode may resolve host names
+        Set<URI> seen = new LinkedHashSet<>();
+        List<URL> answer = new ArrayList<>();
+        addResources(seen, answer, 
camelContext.getClassResolver().loadAllResourcesAsURL(BEAN_PROPERTIES));
+        ClassLoader acl = camelContext.getApplicationContextClassLoader();
+        if (acl != null) {
+            try {
+                addResources(seen, answer, acl.getResources(BEAN_PROPERTIES));
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+        return answer;
+    }
+
+    private static void addResources(Set<URI> seen, List<URL> answer, 
Enumeration<URL> resources) {
+        while (resources != null && resources.hasMoreElements()) {
+            URL url = resources.nextElement();
+            try {
+                if (seen.add(url.toURI())) {
+                    answer.add(url);
+                }
+            } catch (URISyntaxException e) {
+                // ignore
+            }
+        }
+    }
+
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/BeansDeserializer.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/BeansDeserializer.java
index cc616d59e384..b754dc3f802d 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/BeansDeserializer.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/BeansDeserializer.java
@@ -36,6 +36,7 @@ import org.apache.camel.spi.annotations.YamlIn;
 import org.apache.camel.spi.annotations.YamlProperty;
 import org.apache.camel.spi.annotations.YamlType;
 import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.PojoBeanHelper;
 import org.apache.camel.util.ObjectHelper;
 import org.snakeyaml.engine.v2.api.ConstructNode;
 import org.snakeyaml.engine.v2.nodes.Node;
@@ -125,7 +126,7 @@ public class BeansDeserializer extends 
YamlDeserializerSupport implements Constr
             } else {
                 String msg
                         = name != null ? "Error creating bean: " + name + " of 
type: " + type : "Error creating bean: " + type;
-                throw new RuntimeException(msg + classNotFoundHint(e), e);
+                throw new RuntimeException(msg + 
classNotFoundHint(camelContext, e), e);
             }
         }
     }
@@ -192,28 +193,22 @@ public class BeansDeserializer extends 
YamlDeserializerSupport implements Constr
 
     /**
      * The cause of a bean that could not be created is a 
ClassNotFoundException more often than not (a wrong package, a
-     * missing dependency); say so, and for a Camel aggregation strategy 
written in the wrong package name the right
-     * one.
+     * missing dependency); say so, and for a built-in Camel bean written with 
the wrong package name (or with no
+     * package) the right one, from the bean metadata on the classpath.
      */
-    static String classNotFoundHint(Throwable e) {
-        for (Throwable t = e; t != null; t = t.getCause()) {
-            if (t instanceof ClassNotFoundException || t instanceof 
NoClassDefFoundError) {
-                // NoClassDefFoundError names the class in internal form 
(java/lang/Foo)
-                String cls = t.getMessage() != null ? 
t.getMessage().trim().replace('/', '.') : "";
-                String simple = cls.substring(cls.lastIndexOf('.') + 1);
-                String hint = ": class " + cls + " was not found";
-                if (simple.endsWith("AggregationStrategy") && 
!cls.startsWith("org.apache.camel.processor.aggregate.")) {
-                    String candidate = "org.apache.camel.processor.aggregate." 
+ simple;
-                    try {
-                        Class.forName(candidate, false, 
BeansDeserializer.class.getClassLoader());
-                        return hint + " (did you mean " + candidate + "?)";
-                    } catch (Throwable ignore) {
-                        // not one of the built-in strategies
-                    }
-                }
-                return hint + " (check the package name; a class from another 
library needs its dependency added)";
-            }
+    static String classNotFoundHint(CamelContext camelContext, Throwable e) {
+        String cls = PojoBeanHelper.missingClassName(e);
+        if (cls == null) {
+            return "";
         }
-        return "";
+        String hint = ": class " + cls + " was not found";
+        PojoBeanHelper.PojoBean bean = PojoBeanHelper.findByName(camelContext, 
cls);
+        if (bean != null && !bean.javaType().equals(cls)) {
+            return hint + " (did you mean " + bean.javaType()
+                   + (bean.interfaceType() != null ? " (" + 
bean.interfaceType() + ")" : "")
+                   + "? write: type: " + bean.javaType() + ")";
+        }
+        return hint + PojoBeanHelper.classNotFoundHint(camelContext, cls, 
null);
     }
+
 }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/AggregateTest.groovy
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/AggregateTest.groovy
index ff7f670c80bb..23f90ebaeee8 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/AggregateTest.groovy
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/AggregateTest.groovy
@@ -121,4 +121,49 @@ class AggregateTest extends YamlTestSupport {
         then:
         MockEndpoint.assertIsSatisfied(context)
     }
+
+    // CAMEL-24709: a #class: whose class does not exist says which built-in 
strategy was likely meant
+    def 'aggregate (strategy-ref class not found)'() {
+        when:
+        loadRoutes '''
+                - from:
+                    uri: "direct:route"
+                    steps:
+                      - aggregate:
+                          aggregationStrategy: 
"#class:com.foo.UseLatestAggregationStrategy"
+                          completionSize: 2
+                          correlationExpression:
+                            simple: "${header.StockSymbol}"
+                          steps:
+                            - to: "mock:route"
+            '''
+
+        then:
+        def e = thrown(FailedToCreateRouteException)
+        def msg = e.message + '\n' + e.cause?.message
+        msg.contains('No bean could be found in the registry for: 
#class:com.foo.UseLatestAggregationStrategy of type: 
org.apache.camel.AggregationStrategy')
+        msg.contains('did you mean 
org.apache.camel.processor.aggregate.UseLatestAggregationStrategy 
(org.apache.camel.AggregationStrategy)?')
+    }
+
+    def 'aggregate (strategy-ref unknown class lists the built-in 
strategies)'() {
+        when:
+        loadRoutes '''
+                - from:
+                    uri: "direct:route"
+                    steps:
+                      - aggregate:
+                          aggregationStrategy: "#class:com.foo.MyStrategy"
+                          completionSize: 2
+                          correlationExpression:
+                            simple: "${header.StockSymbol}"
+                          steps:
+                            - to: "mock:route"
+            '''
+
+        then:
+        def e = thrown(FailedToCreateRouteException)
+        def msg = e.message + '\n' + e.cause?.message
+        msg.contains('the built-in AggregationStrategy beans are')
+        msg.contains('UseLatestAggregationStrategy 
(org.apache.camel.processor.aggregate.UseLatestAggregationStrategy)')
+    }
 }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/BeansTest.groovy
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/BeansTest.groovy
index 3c3052258259..d5e5f40af62e 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/BeansTest.groovy
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/BeansTest.groovy
@@ -309,4 +309,60 @@ class BeansTest extends YamlTestSupport {
         }
     }
 
+    // CAMEL-24709: a class that was not found names the built-in bean that 
was likely meant, from the bean metadata on the classpath
+    def "beans class not found in wrong package says did you mean"() {
+        when:
+        loadRoutes """
+                - beans:
+                  - name: myAgg
+                    type: com.foo.UseLatestAggregationStrategy
+            """
+
+        then:
+        def e = thrown(Exception)
+        def msg = messages(e)
+        msg.contains('Error creating bean: myAgg of type: 
#class:com.foo.UseLatestAggregationStrategy')
+        msg.contains('class com.foo.UseLatestAggregationStrategy was not 
found')
+        msg.contains('did you mean 
org.apache.camel.processor.aggregate.UseLatestAggregationStrategy 
(org.apache.camel.AggregationStrategy)?')
+        msg.contains('write: type: 
org.apache.camel.processor.aggregate.UseLatestAggregationStrategy')
+    }
+
+    def "beans class not found without package says did you mean"() {
+        when:
+        loadRoutes """
+                - beans:
+                  - name: myRepo
+                    type: MemoryAggregationRepository
+            """
+
+        then:
+        def e = thrown(Exception)
+        def msg = messages(e)
+        msg.contains('class MemoryAggregationRepository was not found')
+        msg.contains('did you mean 
org.apache.camel.processor.aggregate.MemoryAggregationRepository 
(org.apache.camel.spi.AggregationRepository)?')
+    }
+
+    def "beans class not found that is not a built-in bean keeps the generic 
hint"() {
+        when:
+        loadRoutes """
+                - beans:
+                  - name: myBean
+                    type: com.foo.MyBean
+            """
+
+        then:
+        def e = thrown(Exception)
+        def msg = messages(e)
+        msg.contains('class com.foo.MyBean was not found (check the package 
name; a class from another library needs its dependency added)')
+        !msg.contains('did you mean')
+    }
+
+    private static String messages(Throwable e) {
+        def sb = new StringBuilder()
+        for (Throwable t = e; t != null; t = t.cause) {
+            sb.append(t.message).append('\n')
+        }
+        return sb.toString()
+    }
+
 }

Reply via email to