This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new c47a7860f040 CAMEL-24709: camel-yaml-dsl - class not found hint for
beans uses the bean metadata on the classpath (#26455)
c47a7860f040 is described below
commit c47a7860f040f41375134f92abd8e9dff03760d6
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 15 13:55:18 2026 +0200
CAMEL-24709: camel-yaml-dsl - class not found hint for beans uses the bean
metadata on the classpath (#26455)
The class not found hint for beans uses the bean metadata on the classpath,
and camel-core only adds the hint for #class: references.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
.../java/org/apache/camel/NoSuchBeanException.java | 12 +
.../org/apache/camel/reifier/AbstractReifier.java | 24 +-
.../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 | 5 +-
.../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, 648 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..38e648f82ae7 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,10 @@ 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
+ // (a plain bean name that is not in the registry gets no hint as
it is not a class)
+ String hint = name.startsWith("#class:") ?
PojoBeanHelper.classNotFoundHint(camelContext, name, type) : null;
+ throw new NoSuchBeanException(name, type.getName(), hint);
}
return type.cast(obj);
}
@@ -201,6 +205,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..f6051beb83f6 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,10 @@ 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
+ // (a plain bean name that is not in the registry gets no hint
as it is not a class)
+ String hint = value.startsWith("#class:") ?
PojoBeanHelper.classNotFoundHint(context, value, type) : null;
+ throw new NoSuchBeanException(value, type.getTypeName(), hint);
} 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/<Name>.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()
+ }
+
}