This is an automated email from the ASF dual-hosted git repository. Croway pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git
commit b689587a76a1a09374deb0eab089301348513ed2 Author: croway <[email protected]> AuthorDate: Wed Sep 2 15:08:32 2026 +0200 CAMEL-24501: Report starter configuration options that cannot be bound The generated starter code discarded configuration in two places, in both cases without a log line, so an option that never took effect looked exactly like one that did. The generated *ComponentConverter, *DataFormatConverter and *LanguageConverter classes resolve the bean reference that an option of a complex (object) type is configured with. They returned null for any value that did not start with #, and for a value naming a bean that does not exist, so a typo in the bean id produced a component with the option unset. The generated convert() body now delegates to the new BeanReferenceHelper, which resolves #bean:id, #id, a plain bean id, #autowired and #type:fqn, and throws IllegalArgumentException naming the value, the target type and the configuration prefix when the value cannot be resolved. This also drops the per-type switch, which returned null for a target type it did not list. The generated customizers copied the whole configuration onto the target with CamelPropertiesHelper.copyProperties, which binds with failIfNotSet=false, so an option with no matching setter on the target was dropped. The same mojo emits failIfNotSet=true for camel.rest.*, so the two disagreed. The customizers now call the new CamelPropertiesHelper.copyConfigurationProperties, which: - removes the options owned by the auto configuration layer itself (enabled and customizer) before binding, as they are not options on the Camel target; - fails with IllegalArgumentException when an option the application configured itself cannot be set; - logs at DEBUG when an option that only carries its catalog default cannot be set, since the target keeps its own default and there is nothing the application can do about it. Telling the two apart uses the Spring ConfigurationPropertySources, so a catalog default that has never been bindable (camel.language.simple.trim, for example, which is an option of the expression model rather than of SimpleLanguage) does not turn into a startup failure for every application. Blanket strict binding needs the catalog defaults to stop being materialised as field initializers on the configuration classes first, which is left for a follow-up. camel.springboot.lenient-configuration-binding=true logs an explicitly configured option at WARN and continues, instead of failing. The language converter template also referenced an applicationContext field it did not declare; no starter currently generates a language converter, so this was latent. Co-Authored-By: Claude Opus 5 <[email protected]> --- .../HttpComponentBeanReferenceBindingTest.java | 92 ++++++++++++ .../spring/boot/util/BeanReferenceHelper.java | 122 ++++++++++++++++ .../spring/boot/util/CamelPropertiesHelper.java | 158 ++++++++++++++++++++ .../spring/boot/util/BeanReferenceHelperTest.java | 160 +++++++++++++++++++++ .../CamelPropertiesHelperLenientBindingTest.java | 63 ++++++++ .../boot/util/CamelPropertiesHelperTest.java | 110 +++++++++++++- .../maven/SpringBootAutoConfigurationMojo.java | 160 ++++++++------------- .../maven/SpringBootAutoConfigurationMojoTest.java | 76 ++++++++++ 8 files changed, 838 insertions(+), 103 deletions(-) diff --git a/components-starter/camel-http-starter/src/test/java/org/apache/camel/component/http/springboot/HttpComponentBeanReferenceBindingTest.java b/components-starter/camel-http-starter/src/test/java/org/apache/camel/component/http/springboot/HttpComponentBeanReferenceBindingTest.java new file mode 100644 index 00000000000..cc2859adfdd --- /dev/null +++ b/components-starter/camel-http-starter/src/test/java/org/apache/camel/component/http/springboot/HttpComponentBeanReferenceBindingTest.java @@ -0,0 +1,92 @@ +/* + * 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.component.http.springboot; + +import org.apache.camel.support.jsse.SSLContextParameters; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A complex (object) typed option such as {@code sslContextParameters} is configured with a reference to a bean in the + * Spring application context. A value that cannot be resolved must be reported, not silently turned into null. + */ +class HttpComponentBeanReferenceBindingTest { + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TestConfiguration.class)) + .withUserConfiguration(HttpComponentConverter.class); + + @Test + void testBeanSyntaxIsResolved() { + runner.withPropertyValues("camel.component.http.ssl-context-parameters=#bean:mySslContextParameters") + .run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context.getBean(HttpComponentConfiguration.class).getSslContextParameters()) + .isSameAs(context.getBean("mySslContextParameters")); + }); + } + + @Test + void testHashSyntaxIsResolved() { + runner.withPropertyValues("camel.component.http.ssl-context-parameters=#mySslContextParameters") + .run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context.getBean(HttpComponentConfiguration.class).getSslContextParameters()) + .isSameAs(context.getBean("mySslContextParameters")); + }); + } + + @Test + void testPlainBeanIdIsResolved() { + // a plain bean id used to be silently converted to null + runner.withPropertyValues("camel.component.http.ssl-context-parameters=mySslContextParameters") + .run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context.getBean(HttpComponentConfiguration.class).getSslContextParameters()) + .isSameAs(context.getBean("mySslContextParameters")); + }); + } + + @Test + void testUnresolvableValueIsReported() { + // a typo used to leave the option unset without any error + runner.withPropertyValues("camel.component.http.ssl-context-parameters=#bean:mySslContextParametrs") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasStackTraceContaining("mySslContextParametrs") + .hasStackTraceContaining("camel.component.http"); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(HttpComponentConfiguration.class) + static class TestConfiguration { + + @Bean(name = "mySslContextParameters") + SSLContextParameters mySslContextParameters() { + return new SSLContextParameters(); + } + } + +} diff --git a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/BeanReferenceHelper.java b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/BeanReferenceHelper.java new file mode 100644 index 00000000000..63de31c97e5 --- /dev/null +++ b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/BeanReferenceHelper.java @@ -0,0 +1,122 @@ +/* + * 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.spring.boot.util; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.ClassUtils; + +/** + * Resolves the bean references that Camel configuration options of a complex (object) type are configured with in + * <tt>application.properties</tt>, such as + * <tt>camel.component.netty-http.ssl-context-parameters = #bean:mySslContextParameters</tt>. + * <p/> + * This is used by the generated <tt>*ComponentConverter</tt>, <tt>*DataFormatConverter</tt> and + * <tt>*LanguageConverter</tt> classes in the Camel Spring Boot starters. The following syntaxes are supported: + * <ul> + * <li><tt>#bean:myBean</tt> - lookup the bean by its id</li> + * <li><tt>#myBean</tt> - lookup the bean by its id</li> + * <li><tt>myBean</tt> - lookup the bean by its id</li> + * <li><tt>#autowired</tt> - lookup the single bean of the option type</li> + * <li><tt>#type:com.foo.MyType</tt> - lookup the single bean of the given type</li> + * </ul> + * A configured value that cannot be resolved is reported by throwing an {@link IllegalArgumentException}, so a typo in + * a configuration file is not silently turned into a <tt>null</tt> value. + */ +public final class BeanReferenceHelper { + + private static final String BEAN_PREFIX = "#bean:"; + private static final String TYPE_PREFIX = "#type:"; + private static final String CLASS_PREFIX = "#class:"; + private static final String AUTOWIRED = "#autowired"; + + private BeanReferenceHelper() { + } + + /** + * Resolves the given configured value as a bean from the Spring application context. + * + * @param applicationContext + * the Spring application context + * @param source + * the configured value, such as <tt>#bean:myBean</tt> + * @param targetType + * the type the option expects + * @param propertyPrefix + * the configuration prefix the option belongs to, such as + * <tt>camel.component.netty-http</tt>, used to make the error message actionable + * + * @return the resolved bean, or <tt>null</tt> if no value was configured + * + * @throws IllegalArgumentException + * if a value was configured but cannot be resolved to a bean of the target type + */ + public static Object resolveBeanReference(ApplicationContext applicationContext, Object source, + TypeDescriptor targetType, String propertyPrefix) { + if (source == null) { + return null; + } + String value = source.toString().trim(); + if (value.isEmpty()) { + return null; + } + Class<?> type = targetType != null ? targetType.getObjectType() : Object.class; + if (applicationContext == null) { + throw new IllegalArgumentException( + message(value, type, propertyPrefix, "there is no Spring application context available")); + } + if (value.startsWith(CLASS_PREFIX)) { + throw new IllegalArgumentException(message(value, type, propertyPrefix, + "the #class: syntax is not supported here, declare the bean in the Spring application context instead")); + } + try { + if (AUTOWIRED.equalsIgnoreCase(value)) { + return applicationContext.getBean(type); + } + if (value.startsWith(TYPE_PREFIX)) { + String fqn = value.substring(TYPE_PREFIX.length()).trim(); + return applicationContext.getBean(ClassUtils.forName(fqn, applicationContext.getClassLoader())); + } + String id = value; + if (id.startsWith(BEAN_PREFIX)) { + id = id.substring(BEAN_PREFIX.length()).trim(); + } else if (id.startsWith("#")) { + id = id.substring(1).trim(); + } + if (id.isEmpty()) { + throw new IllegalArgumentException(message(value, type, propertyPrefix, "the bean id is empty")); + } + return applicationContext.getBean(id, type); + } catch (BeansException | ClassNotFoundException | LinkageError e) { + throw new IllegalArgumentException(message(value, type, propertyPrefix, e.getMessage()), e); + } + } + + private static String message(String value, Class<?> type, String propertyPrefix, String reason) { + StringBuilder sb = new StringBuilder(256); + sb.append("Cannot resolve value [").append(value).append("] as a bean of type [").append(type.getName()) + .append("]"); + if (propertyPrefix != null && !propertyPrefix.isEmpty()) { + sb.append(" while configuring ").append(propertyPrefix).append(".*"); + } + sb.append(". Use #bean:myBeanId (or #myBeanId, or myBeanId) to refer to a bean in the Spring application" + + " context, or #autowired or #type:com.foo.MyType to refer to it by type. Reason: ").append(reason); + return sb.toString(); + } + +} diff --git a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/CamelPropertiesHelper.java b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/CamelPropertiesHelper.java index 31d2b05cdaa..66b1601fd29 100644 --- a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/CamelPropertiesHelper.java +++ b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/util/CamelPropertiesHelper.java @@ -16,27 +16,174 @@ */ package org.apache.camel.spring.boot.util; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Optional; +import java.util.Set; import org.apache.camel.CamelContext; import org.apache.camel.Component; import org.apache.camel.PropertyBindingException; import org.apache.camel.spi.BeanIntrospection; +import org.apache.camel.spi.PropertiesComponent; import org.apache.camel.spi.PropertyConfigurer; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.PropertyBindingSupport; import org.apache.camel.support.service.ServiceHelper; import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.StringHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.context.ApplicationContext; /** * To help configuring Camel properties that have been defined in Spring Boot configuration files. */ public final class CamelPropertiesHelper { + /** + * Property to turn off failing fast when an explicitly configured Spring Boot option cannot be set on the Camel + * component, data format or language it belongs to. When lenient, such an option is logged at WARN level and + * ignored, which is close to the behaviour before Camel 4.23, where it was dropped without any log line. + */ + public static final String LENIENT_CONFIGURATION_BINDING = "camel.springboot.lenient-configuration-binding"; + + private static final Logger LOG = LoggerFactory.getLogger(CamelPropertiesHelper.class); + + /** + * Options that belong to the Spring Boot auto configuration layer itself, and therefore are not options on the + * Camel component, data format or language being configured. + */ + private static final Set<String> AUTO_CONFIGURATION_OPTIONS = Set.of("enabled", "customizer"); + private CamelPropertiesHelper() { } + /** + * Copies the options from a generated Spring Boot configuration class onto the Camel component, data format or + * language it configures. + * <p/> + * The options that belong to the auto configuration layer itself (<tt>enabled</tt> and <tt>customizer</tt>) are + * removed first, as they are not options on the target bean. + * <p/> + * An option that cannot be set on the target and that the application configured explicitly fails fast with an + * {@link IllegalArgumentException}, instead of being dropped without a trace. An option that cannot be set and + * that only carries its catalog default is logged at DEBUG, as there is nothing the application can do about it + * and the target keeps its own default. Set {@link #LENIENT_CONFIGURATION_BINDING} to <tt>true</tt> to log an + * explicitly configured option at WARN and continue, instead of failing. + * + * @param camelContext + * the CamelContext + * @param applicationContext + * the Spring application context, used to tell an explicitly configured option from a + * catalog default + * @param propertyPrefix + * the configuration prefix of the source, such as <tt>camel.component.http</tt> + * @param source + * the Spring Boot configuration class + * @param target + * the Camel component, data format or language to configure + */ + public static void copyConfigurationProperties(CamelContext camelContext, ApplicationContext applicationContext, + String propertyPrefix, Object source, Object target) { + ObjectHelper.notNull(camelContext, "camel context"); + ObjectHelper.notNull(source, "source"); + ObjectHelper.notNull(target, "target"); + + Map<String, Object> properties = getNonNullProperties(camelContext, source); + properties.keySet().removeIf(key -> AUTO_CONFIGURATION_OPTIONS.contains(key.toLowerCase(Locale.US))); + + // the options that could be set are removed from the map, so what is left could not be set + doSetCamelProperties(camelContext, target, properties, false, false); + if (properties.isEmpty()) { + return; + } + + boolean lenient = isLenientBinding(camelContext); + List<String> failed = new ArrayList<>(); + for (Map.Entry<String, Object> entry : properties.entrySet()) { + String name = entry.getKey(); + Object value = entry.getValue(); + if (isExplicitlyConfigured(applicationContext, propertyPrefix, name)) { + if (lenient) { + LOG.warn("Cannot configure option [{}] with value [{}] on [{}]. This option is ignored.", + optionKey(propertyPrefix, name), value, ObjectHelper.classCanonicalName(target)); + } else { + failed.add(optionKey(propertyPrefix, name) + " = " + value); + } + } else { + // only the catalog default was carried, so the target keeps its own default + LOG.debug("Cannot configure option [{}] with default value [{}] on [{}]. This option is ignored.", + optionKey(propertyPrefix, name), value, ObjectHelper.classCanonicalName(target)); + } + } + if (!failed.isEmpty()) { + throw new IllegalArgumentException( + "Cannot configure " + failed + " as the bean class [" + ObjectHelper.classCanonicalName(target) + + "] has no suitable setter method, or it is not possible to lookup a bean with that id in the" + + " Spring Boot registry. Remove or correct the option, or set " + + LENIENT_CONFIGURATION_BINDING + "=true to ignore it."); + } + } + + private static String optionKey(String propertyPrefix, String name) { + String dashed = StringHelper.camelCaseToDash(name); + return propertyPrefix != null && !propertyPrefix.isEmpty() ? propertyPrefix + "." + dashed : dashed; + } + + /** + * Whether the application configured the given option itself, as opposed to the option only carrying the default + * value the generator took from the Camel catalog. + */ + private static boolean isExplicitlyConfigured(ApplicationContext applicationContext, String propertyPrefix, + String name) { + if (applicationContext == null || propertyPrefix == null || propertyPrefix.isEmpty()) { + return false; + } + try { + ConfigurationPropertyName key + = ConfigurationPropertyName.of(optionKey(propertyPrefix, name).toLowerCase(Locale.US)); + for (ConfigurationPropertySource source : ConfigurationPropertySources + .get(applicationContext.getEnvironment())) { + if (source.getConfigurationProperty(key) != null) { + return true; + } + } + } catch (Exception e) { + LOG.debug("Cannot determine whether {}.{} was configured due to: {}", propertyPrefix, name, + e.getMessage()); + } + return false; + } + + private static boolean isLenientBinding(CamelContext camelContext) { + try { + PropertiesComponent pc = camelContext.getPropertiesComponent(); + if (pc != null) { + Optional<String> value = pc.resolveProperty(LENIENT_CONFIGURATION_BINDING); + if (value.isPresent()) { + return "true".equalsIgnoreCase(value.get().trim()); + } + } + } catch (Exception e) { + LOG.debug("Cannot resolve {} due to: {}. Using strict configuration binding.", + LENIENT_CONFIGURATION_BINDING, e.getMessage()); + } + return false; + } + + /** + * Copies all non-null options from the source onto the target, ignoring any option that cannot be set. + * + * @see #copyConfigurationProperties(CamelContext, ApplicationContext, String, Object, Object) for copying a + * generated Spring Boot configuration class onto the Camel bean it configures + */ @SuppressWarnings({ "unchecked" }) public static void copyProperties(CamelContext camelContext, Object source, Object target) { ObjectHelper.notNull(camelContext, "camel context"); @@ -94,6 +241,11 @@ public final class CamelPropertiesHelper { */ public static boolean setCamelProperties(CamelContext context, Object target, Map<String, Object> properties, boolean failIfNotSet) { + return doSetCamelProperties(context, target, properties, failIfNotSet, true); + } + + private static boolean doSetCamelProperties(CamelContext context, Object target, Map<String, Object> properties, + boolean failIfNotSet, boolean warnIfNotSet) { ObjectHelper.notNull(context, "context"); ObjectHelper.notNull(target, "target"); ObjectHelper.notNull(properties, "properties"); @@ -139,6 +291,12 @@ public final class CamelPropertiesHelper { + "] as the bean class [" + ObjectHelper.classCanonicalName(target) + "] has no suitable setter method, or not possible to lookup a bean with the id [" + stringValue + "] in Spring Boot registry"); + } else if (warnIfNotSet) { + LOG.warn( + "Cannot configure option [{}] with value [{}] as the bean class [{}] has no suitable setter method," + + " or not possible to lookup a bean with the id [{}] in Spring Boot registry." + + " This option is ignored.", + name, stringValue, ObjectHelper.classCanonicalName(target), stringValue); } } diff --git a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/BeanReferenceHelperTest.java b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/BeanReferenceHelperTest.java new file mode 100644 index 00000000000..ac8b807c82a --- /dev/null +++ b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/BeanReferenceHelperTest.java @@ -0,0 +1,160 @@ +/* + * 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.spring.boot.util; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.TypeDescriptor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BeanReferenceHelperTest { + + private static final String PREFIX = "camel.component.myComponent"; + + private AnnotationConfigApplicationContext applicationContext; + + public static class MyOption { + } + + public static class MyOtherOption { + } + + @Configuration + static class TestConfiguration { + @Bean(name = "myOption") + MyOption myOption() { + return new MyOption(); + } + + @Bean(name = "myOtherOption") + MyOtherOption myOtherOption() { + return new MyOtherOption(); + } + } + + @BeforeEach + public void setUp() { + applicationContext = new AnnotationConfigApplicationContext(TestConfiguration.class); + } + + @AfterEach + public void tearDown() { + if (applicationContext != null) { + applicationContext.close(); + } + } + + private Object resolve(Object source) { + return BeanReferenceHelper.resolveBeanReference(applicationContext, source, + TypeDescriptor.valueOf(MyOption.class), PREFIX); + } + + @Test + public void testNullAndEmptyAreNotConfigured() { + assertNull(resolve(null)); + assertNull(resolve("")); + assertNull(resolve(" ")); + } + + @Test + public void testBeanSyntax() { + assertSame(applicationContext.getBean("myOption"), resolve("#bean:myOption")); + } + + @Test + public void testHashSyntax() { + assertSame(applicationContext.getBean("myOption"), resolve("#myOption")); + } + + @Test + public void testPlainBeanId() { + // a plain bean id used to be silently converted to null + assertSame(applicationContext.getBean("myOption"), resolve("myOption")); + } + + @Test + public void testAutowired() { + assertSame(applicationContext.getBean("myOption"), resolve("#autowired")); + } + + @Test + public void testType() { + assertSame(applicationContext.getBean("myOption"), + resolve("#type:" + MyOption.class.getName())); + } + + @Test + public void testUnknownBeanFailsClosed() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> resolve("#bean:noSuchBean")); + assertTrue(e.getMessage().contains("#bean:noSuchBean"), e.getMessage()); + assertTrue(e.getMessage().contains(MyOption.class.getName()), e.getMessage()); + assertTrue(e.getMessage().contains(PREFIX), e.getMessage()); + assertTrue(e.getMessage().contains("#bean:myBeanId"), e.getMessage()); + } + + @Test + public void testTypoedPlainValueFailsClosed() { + // this is the case that used to end up as a null value on the component + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> resolve("myOptionn")); + assertTrue(e.getMessage().contains("myOptionn"), e.getMessage()); + } + + @Test + public void testWrongBeanTypeFailsClosed() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> resolve("#myOtherOption")); + assertTrue(e.getMessage().contains("myOtherOption"), e.getMessage()); + assertTrue(e.getMessage().contains(MyOption.class.getName()), e.getMessage()); + } + + @Test + public void testEmptyBeanIdFailsClosed() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> resolve("#bean:")); + assertTrue(e.getMessage().contains("the bean id is empty"), e.getMessage()); + } + + @Test + public void testClassSyntaxIsReported() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> resolve("#class:" + MyOption.class.getName())); + assertTrue(e.getMessage().contains("#class: syntax is not supported"), e.getMessage()); + } + + @Test + public void testUnknownTypeFailsClosed() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> resolve("#type:com.foo.DoesNotExist")); + assertTrue(e.getMessage().contains("com.foo.DoesNotExist"), e.getMessage()); + } + + @Test + public void testMessageMentionsTheTargetType() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> BeanReferenceHelper.resolveBeanReference(applicationContext, "nope", + TypeDescriptor.valueOf(MyOtherOption.class), PREFIX)); + assertEquals(true, e.getMessage().contains(MyOtherOption.class.getName())); + } + +} diff --git a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperLenientBindingTest.java b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperLenientBindingTest.java new file mode 100644 index 00000000000..31048904693 --- /dev/null +++ b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperLenientBindingTest.java @@ -0,0 +1,63 @@ +/* + * 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.spring.boot.util; + +import org.apache.camel.CamelContext; +import org.apache.camel.test.spring.junit6.CamelSpringBootTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.test.annotation.DirtiesContext; + +/** + * Verifies that {@link CamelPropertiesHelper#LENIENT_CONFIGURATION_BINDING} restores the pre Camel 4.23 behaviour of + * ignoring options that cannot be set on the Camel bean being configured. + */ +@CamelSpringBootTest +@DirtiesContext +@SpringBootApplication +@SpringBootTest( + classes = { CamelPropertiesHelperLenientBindingTest.class }, + properties = { + "camel.springboot.lenient-configuration-binding = true", + "camel.test.my-config.no-such-option-on-the-target = bar" }) +public class CamelPropertiesHelperLenientBindingTest { + + @Autowired + ApplicationContext applicationContext; + + @Autowired + CamelContext camelContext; + + @Test + public void testConfiguredOptionThatCannotBeSetIsIgnoredWhenLenient() { + CamelPropertiesHelperTest.MyClass target = new CamelPropertiesHelperTest.MyClass(); + + CamelPropertiesHelperTest.MyDriftedConfiguration config = new CamelPropertiesHelperTest.MyDriftedConfiguration(); + config.setName("Donald Duck"); + config.setNoSuchOptionOnTheTarget("bar"); + + CamelPropertiesHelper.copyConfigurationProperties(camelContext, applicationContext, + CamelPropertiesHelperTest.PREFIX, config, target); + + Assertions.assertEquals("Donald Duck", target.getName()); + } + +} diff --git a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperTest.java b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperTest.java index 3ef331b4d1b..d47d3eb07ff 100644 --- a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperTest.java +++ b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/util/CamelPropertiesHelperTest.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import org.apache.camel.CamelContext; +import org.apache.camel.spring.boot.ComponentConfigurationPropertiesCommon; import org.apache.camel.test.spring.junit6.CamelSpringBootTest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -34,9 +35,13 @@ import org.springframework.test.annotation.DirtiesContext; @CamelSpringBootTest @DirtiesContext @SpringBootApplication -@SpringBootTest(classes = { CamelPropertiesHelperTest.TestConfiguration.class }) +@SpringBootTest( + classes = { CamelPropertiesHelperTest.TestConfiguration.class }, + properties = { "camel.test.my-config.no-such-option-on-the-target = bar" }) public class CamelPropertiesHelperTest { + static final String PREFIX = "camel.test.my-config"; + @Autowired ApplicationContext context; @@ -54,6 +59,58 @@ public class CamelPropertiesHelperTest { public static class MyOption { } + /** + * Mimics a generated {@code *ComponentConfiguration} class: the auto configuration layer options + * (enabled/customizer) are inherited and are not options on the Camel target bean. + */ + public static class MyConfiguration extends ComponentConfigurationPropertiesCommon { + + private String name; + private MyOption option; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public MyOption getOption() { + return option; + } + + public void setOption(MyOption option) { + this.option = option; + } + } + + /** + * A configuration class holding an option that does not exist on the target bean, which is what generator or + * catalog drift looks like at runtime. + */ + public static class MyDriftedConfiguration extends MyConfiguration { + + private String noSuchOptionOnTheTarget; + private String anotherOptionOnlyCarryingItsDefault; + + public String getNoSuchOptionOnTheTarget() { + return noSuchOptionOnTheTarget; + } + + public void setNoSuchOptionOnTheTarget(String noSuchOptionOnTheTarget) { + this.noSuchOptionOnTheTarget = noSuchOptionOnTheTarget; + } + + public String getAnotherOptionOnlyCarryingItsDefault() { + return anotherOptionOnlyCarryingItsDefault; + } + + public void setAnotherOptionOnlyCarryingItsDefault(String anotherOptionOnlyCarryingItsDefault) { + this.anotherOptionOnlyCarryingItsDefault = anotherOptionOnlyCarryingItsDefault; + } + } + public static class MyClass { private int id; @@ -226,6 +283,57 @@ public class CamelPropertiesHelperTest { Assertions.assertSame(context.getBean("myCoolOption"), target.getOption()); } + @Test + public void testCopyConfigurationPropertiesIgnoresAutoConfigurationOptions() { + MyClass target = new MyClass(); + + MyConfiguration config = new MyConfiguration(); + config.setName("Donald Duck"); + config.setOption(context.getBean("myCoolOption", MyOption.class)); + + // enabled and customizer are always set on a generated configuration class, and must not be + // attempted on the target bean + Assertions.assertTrue(config.isEnabled()); + Assertions.assertNotNull(config.getCustomizer()); + + CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, config, target); + + Assertions.assertEquals("Donald Duck", target.getName()); + Assertions.assertSame(context.getBean("myCoolOption"), target.getOption()); + } + + @Test + public void testCopyConfigurationPropertiesFailsOnConfiguredOptionThatCannotBeSet() { + MyClass target = new MyClass(); + + MyDriftedConfiguration config = new MyDriftedConfiguration(); + config.setName("Donald Duck"); + // camel.test.my-config.no-such-option-on-the-target is set on the test application + config.setNoSuchOptionOnTheTarget("bar"); + + IllegalArgumentException e = Assertions.assertThrows(IllegalArgumentException.class, + () -> CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, config, + target)); + Assertions.assertTrue(e.getMessage().contains("camel.test.my-config.no-such-option-on-the-target"), + e.getMessage()); + Assertions.assertTrue(e.getMessage().contains(CamelPropertiesHelper.LENIENT_CONFIGURATION_BINDING), + e.getMessage()); + } + + @Test + public void testCopyConfigurationPropertiesIgnoresDefaultThatCannotBeSet() { + MyClass target = new MyClass(); + + MyDriftedConfiguration config = new MyDriftedConfiguration(); + config.setName("Donald Duck"); + // nothing configured this one, so it only carries a catalog default and must not break startup + config.setAnotherOptionOnlyCarryingItsDefault("false"); + + CamelPropertiesHelper.copyConfigurationProperties(camelContext, context, PREFIX, config, target); + + Assertions.assertEquals("Donald Duck", target.getName()); + } + @Test public void testSetCamelPropertiesUnknownOptionIgnore() throws Exception { MyClass target = new MyClass(); diff --git a/tooling/camel-spring-boot-generator-maven-plugin/src/main/java/org/apache/camel/springboot/maven/SpringBootAutoConfigurationMojo.java b/tooling/camel-spring-boot-generator-maven-plugin/src/main/java/org/apache/camel/springboot/maven/SpringBootAutoConfigurationMojo.java index 554c408f7bf..f9888d27724 100644 --- a/tooling/camel-spring-boot-generator-maven-plugin/src/main/java/org/apache/camel/springboot/maven/SpringBootAutoConfigurationMojo.java +++ b/tooling/camel-spring-boot-generator-maven-plugin/src/main/java/org/apache/camel/springboot/maven/SpringBootAutoConfigurationMojo.java @@ -554,7 +554,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator createComponentConfigurationSource(pkg, model, overrideComponentName); createComponentAutoConfigurationSource(pkg, model, overrideComponentName, complexOptions); if (complexOptions) { - createComponentConverterSource(pkg, model); + createComponentConverterSource(pkg, model, overrideComponentName); } createComponentSpringFactorySource(pkg, model); } @@ -621,7 +621,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator createDataFormatConfigurationSource(pkg, model, overrideDataFormatName); createDataFormatAutoConfigurationSource(pkg, model, overrideDataFormatName, complexOptions); if (complexOptions) { - createDataFormatConverterSource(pkg, model); + createDataFormatConverterSource(pkg, model, overrideDataFormatName); } createDataFormatSpringFactorySource(pkg, model); } @@ -672,7 +672,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator createLanguageConfigurationSource(pkg, model, overrideLanguageName); createLanguageAutoConfigurationSource(pkg, model, overrideLanguageName, complexOptions); if (complexOptions) { - createLanguageConverterSource(pkg, model); + createLanguageConverterSource(pkg, model, overrideLanguageName); } createLanguageSpringFactorySource(pkg, model); } @@ -706,10 +706,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator } javaClass.getJavaDoc().setText(doc); - String prefix = "camel.component." - + camelCaseToDash(overrideComponentName != null ? overrideComponentName : model.getScheme()); - // make sure prefix is in lower case - prefix = prefix.toLowerCase(Locale.US); + String prefix = componentPropertyPrefix(model, overrideComponentName); javaClass.addAnnotation("org.springframework.boot.context.properties.ConfigurationProperties") .setStringValue("prefix", prefix); @@ -1162,10 +1159,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator } javaClass.getJavaDoc().setFullText(doc); - String prefix = "camel.dataformat." - + camelCaseToDash(overrideDataFormatName != null ? overrideDataFormatName : model.getName()); - // make sure prefix is in lower case - prefix = prefix.toLowerCase(Locale.US); + String prefix = dataFormatPropertyPrefix(model, overrideDataFormatName); javaClass.addAnnotation("org.springframework.boot.context.properties.ConfigurationProperties") .setStringValue("prefix", prefix); @@ -1269,10 +1263,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator } javaClass.getJavaDoc().setFullText(doc); - String prefix = "camel.language." - + (camelCaseToDash(overrideLanguageName != null ? overrideLanguageName : model.getName())); - // make sure prefix is in lower case - prefix = prefix.toLowerCase(Locale.US); + String prefix = languagePropertyPrefix(model, overrideLanguageName); javaClass.addAnnotation("org.springframework.boot.context.properties.ConfigurationProperties") .setStringValue("prefix", prefix); @@ -1442,10 +1433,12 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator writeSourceIfChanged(javaClass, fileName, false); } - private void createComponentConverterSource(String packageName, ComponentModel model) throws MojoFailureException { + private void createComponentConverterSource(String packageName, ComponentModel model, String overrideComponentName) + throws MojoFailureException { final String name = model.getJavaType().substring(model.getJavaType().lastIndexOf(".") + 1).replace("Component", "ComponentConverter"); + final String prefix = componentPropertyPrefix(model, overrideComponentName); // create converter class and write source JavaClass javaClass = new JavaClass(getProjectClassLoader()); @@ -1457,6 +1450,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator javaClass.addAnnotation("org.springframework.stereotype.Component"); javaClass.addImport("java.util.LinkedHashSet"); javaClass.addImport("java.util.Set"); + javaClass.addImport("org.apache.camel.spring.boot.util.BeanReferenceHelper"); javaClass.addImport("org.springframework.core.convert.TypeDescriptor"); javaClass.addImport("org.springframework.core.convert.converter.GenericConverter"); @@ -1467,7 +1461,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator String body = createConverterPairBody(model); javaClass.addMethod().setName("getConvertibleTypes").setPublic().setReturnType("Set<ConvertiblePair>") .setBody(body); - body = createConvertBody(model); + body = createConvertBody(prefix); javaClass.addMethod().setName("convert").setPublic().setReturnType("Object").addParameter("Object", "source") .addParameter("TypeDescriptor", "sourceType").addParameter("TypeDescriptor", "targetType") .setBody(body); @@ -1479,76 +1473,32 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator writeComponentSpringFactorySource(packageName, name); } - private String createConvertBody(ComponentModel model) { - StringBuilder sb = new StringBuilder(); - sb.append("if (source == null) {\n"); - sb.append(" return null;\n"); - sb.append("}\n"); - sb.append("String ref = source.toString();\n"); - sb.append("if (!ref.startsWith(\"#\")) {\n"); - sb.append(" return null;\n"); - sb.append("}\n"); - sb.append("ref = ref.startsWith(\"#bean:\") ? ref.substring(6) : ref.substring(1);\n"); - sb.append("switch (targetType.getName()) {\n"); - // we need complex types only which unique types only - Stream<String> s = model.getComponentOptions().stream().filter(this::isComplexType) - .map(SpringBootAutoConfigurationMojo::getJavaType).distinct(); - s.forEach(type -> { - String replacedType = applyTypeReplacement(type); - sb.append(" case \"").append(replacedType).append("\": return applicationContext.getBean(ref, ").append(replacedType) - .append(".class);\n"); - }); - sb.append("}\n"); - sb.append("return null;\n"); - return sb.toString(); + private static String componentPropertyPrefix(ComponentModel model, String overrideComponentName) { + return ("camel.component." + + camelCaseToDash(overrideComponentName != null ? overrideComponentName : model.getScheme())) + .toLowerCase(Locale.US); } - private String createConvertBody(DataFormatModel model) { - StringBuilder sb = new StringBuilder(); - sb.append("if (source == null) {\n"); - sb.append(" return null;\n"); - sb.append("}\n"); - sb.append("String ref = source.toString();\n"); - sb.append("if (!ref.startsWith(\"#\")) {\n"); - sb.append(" return null;\n"); - sb.append("}\n"); - sb.append("ref = ref.startsWith(\"#bean:\") ? ref.substring(6) : ref.substring(1);\n"); - sb.append("switch (targetType.getName()) {\n"); - // we need complex types only which unique types only - Stream<String> s = model.getOptions().stream().filter(this::isComplexType) - .map(SpringBootAutoConfigurationMojo::getJavaType).distinct(); - s.forEach(type -> { - String replacedType = applyTypeReplacement(type); - sb.append(" case \"").append(replacedType).append("\": return applicationContext.getBean(ref, ").append(replacedType) - .append(".class);\n"); - }); - sb.append("}\n"); - sb.append("return null;\n"); - return sb.toString(); + private static String dataFormatPropertyPrefix(DataFormatModel model, String overrideDataFormatName) { + return ("camel.dataformat." + + camelCaseToDash(overrideDataFormatName != null ? overrideDataFormatName : model.getName())) + .toLowerCase(Locale.US); } - private String createConvertBody(LanguageModel model) { - StringBuilder sb = new StringBuilder(); - sb.append("if (source == null) {\n"); - sb.append(" return null;\n"); - sb.append("}\n"); - sb.append("String ref = source.toString();\n"); - sb.append("if (!ref.startsWith(\"#\")) {\n"); - sb.append(" return null;\n"); - sb.append("}\n"); - sb.append("ref = ref.startsWith(\"#bean:\") ? ref.substring(6) : ref.substring(1);\n"); - sb.append("switch (targetType.getName()) {\n"); - // we need complex types only which unique types only - Stream<String> s = model.getOptions().stream().filter(this::isComplexType) - .map(SpringBootAutoConfigurationMojo::getJavaType).distinct(); - s.forEach(type -> { - String replacedType = applyTypeReplacement(type); - sb.append(" case \"").append(replacedType).append("\": return applicationContext.getBean(ref, ").append(replacedType) - .append(".class);\n"); - }); - sb.append("}\n"); - sb.append("return null;\n"); - return sb.toString(); + private static String languagePropertyPrefix(LanguageModel model, String overrideLanguageName) { + return ("camel.language." + + camelCaseToDash(overrideLanguageName != null ? overrideLanguageName : model.getName())) + .toLowerCase(Locale.US); + } + + /** + * The body of the generated converters. The value is resolved by + * {@code org.apache.camel.spring.boot.util.BeanReferenceHelper} which fails with a meaningful error when a + * configured value cannot be resolved, instead of silently converting it to null. + */ + static String createConvertBody(String propertyPrefix) { + return "return BeanReferenceHelper.resolveBeanReference(applicationContext, source, targetType, \"" + + propertyPrefix + "\");\n"; } private String createConverterPairBody(ComponentModel model) { @@ -1662,11 +1612,12 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator writeSourceIfChanged(javaClass, fileName, false); } - private void createDataFormatConverterSource(String packageName, DataFormatModel model) - throws MojoFailureException { + private void createDataFormatConverterSource(String packageName, DataFormatModel model, + String overrideDataFormatName) throws MojoFailureException { final String name = model.getJavaType().substring(model.getJavaType().lastIndexOf(".") + 1) .replace("DataFormat", "DataFormatConverter"); + final String prefix = dataFormatPropertyPrefix(model, overrideDataFormatName); // create converter class and write source JavaClass javaClass = new JavaClass(getProjectClassLoader()); @@ -1678,7 +1629,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator javaClass.addAnnotation("org.springframework.stereotype.Component"); javaClass.addImport("java.util.LinkedHashSet"); javaClass.addImport("java.util.Set"); - javaClass.addImport("org.apache.camel.CamelContext"); + javaClass.addImport("org.apache.camel.spring.boot.util.BeanReferenceHelper"); javaClass.addImport("org.springframework.core.convert.TypeDescriptor"); javaClass.addImport("org.springframework.core.convert.converter.GenericConverter"); @@ -1689,7 +1640,7 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator String body = createConverterPairBody(model); javaClass.addMethod().setName("getConvertibleTypes").setPublic().setReturnType("Set<ConvertiblePair>") .setBody(body); - body = createConvertBody(model); + body = createConvertBody(prefix); javaClass.addMethod().setName("convert").setPublic().setReturnType("Object").addParameter("Object", "source") .addParameter("TypeDescriptor", "sourceType").addParameter("TypeDescriptor", "targetType") .setBody(body); @@ -1765,33 +1716,35 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator writeComponentSpringFactorySource(packageName, name); } - private void createLanguageConverterSource(String packageName, LanguageModel model) throws MojoFailureException { + private void createLanguageConverterSource(String packageName, LanguageModel model, String overrideLanguageName) + throws MojoFailureException { final String name = model.getJavaType().substring(model.getJavaType().lastIndexOf(".") + 1).replace("Language", "LanguageConverter"); + final String prefix = languagePropertyPrefix(model, overrideLanguageName); // create converter class and write source JavaClass javaClass = new JavaClass(getProjectClassLoader()); javaClass.setPackage(packageName); javaClass.setName(name); javaClass.getJavaDoc().setFullText("Generated by camel-package-maven-plugin - do not edit this file!"); + javaClass.addAnnotation(Configuration.class).setLiteralValue("proxyBeanMethods", "false"); + javaClass.addAnnotation("org.springframework.boot.context.properties.ConfigurationPropertiesBinding"); + javaClass.addAnnotation("org.springframework.stereotype.Component"); javaClass.addImport("java.util.LinkedHashSet"); javaClass.addImport("java.util.Set"); - javaClass.addImport("org.apache.camel.CamelContext"); + javaClass.addImport("org.apache.camel.spring.boot.util.BeanReferenceHelper"); javaClass.addImport("org.springframework.core.convert.TypeDescriptor"); javaClass.addImport("org.springframework.core.convert.converter.GenericConverter"); - javaClass.implementInterface("org.springframework.core.convert.converter.GenericConverter"); - javaClass.addField().setPrivate().setFinal(true).setName("camelContext") - .setType(loadClass("org.apache.camel.CamelContext")); - javaClass.addMethod().setConstructor(true).setPublic().setName(name) - .addParameter("org.apache.camel.CamelContext", "camelContext") - .setBody("this.camelContext = camelContext;\n"); + javaClass.implementInterface("GenericConverter"); + javaClass.addField().setPrivate().setName("applicationContext") + .setType(loadClass("org.springframework.context.ApplicationContext")).addAnnotation(Autowired.class); String body = createConverterPairBody(model); javaClass.addMethod().setName("getConvertibleTypes").setPublic().setReturnType("Set<ConvertiblePair>") .setBody(body); - body = createConvertBody(model); + body = createConvertBody(prefix); javaClass.addMethod().setName("convert").setPublic().setReturnType("Object").addParameter("Object", "source") .addParameter("TypeDescriptor", "sourceType").addParameter("TypeDescriptor", "targetType") .setBody(body); @@ -1819,10 +1772,11 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator writeComponentSpringFactorySource(packageName, name); } - private static String createComponentBody(String shortJavaType, String name) { + static String createComponentBody(String shortJavaType, String name) { return new StringBuilder().append("return new ComponentCustomizer() {\n").append(" @Override\n") .append(" public void configure(String name, Component target) {\n") - .append(" CamelPropertiesHelper.copyProperties(target.getCamelContext(), configuration, target);\n") + .append(" CamelPropertiesHelper.copyConfigurationProperties(target.getCamelContext(), applicationContext,\n") + .append(" \"camel.component.").append(name).append("\", configuration, target);\n") .append(" }\n").append(" @Override\n") .append(" public boolean isEnabled(String name, Component target) {\n") .append(" return HierarchicalPropertiesEvaluator.evaluate(\n") @@ -1833,10 +1787,11 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator .append("};\n").toString(); } - private static String createDataFormatBody(String shortJavaType, String name) { + static String createDataFormatBody(String shortJavaType, String name) { return new StringBuilder().append("return new DataFormatCustomizer() {\n").append(" @Override\n") .append(" public void configure(String name, DataFormat target) {\n") - .append(" CamelPropertiesHelper.copyProperties(camelContextProvider.getObject(), configuration, target);\n") + .append(" CamelPropertiesHelper.copyConfigurationProperties(camelContextProvider.getObject(), applicationContext,\n") + .append(" \"camel.dataformat.").append(name).append("\", configuration, target);\n") .append(" }\n").append(" @Override\n") .append(" public boolean isEnabled(String name, DataFormat target) {\n") .append(" return HierarchicalPropertiesEvaluator.evaluate(\n") @@ -1847,11 +1802,12 @@ public class SpringBootAutoConfigurationMojo extends AbstractSpringBootGenerator .append("};\n").toString(); } - private static String createLanguageBody(String shortJavaType, String name) { + static String createLanguageBody(String shortJavaType, String name) { return new StringBuilder().append("return new LanguageCustomizer() {\n").append(" @Override\n") .append(" public void configure(String name, Language target) {\n") .append(" if (target instanceof CamelContextAware cca && cca.getCamelContext() != null) {\n") - .append(" CamelPropertiesHelper.copyProperties(cca.getCamelContext(), configuration, target);\n") + .append(" CamelPropertiesHelper.copyConfigurationProperties(cca.getCamelContext(), applicationContext,\n") + .append(" \"camel.language.").append(name).append("\", configuration, target);\n") .append(" } else {\n") .append(" org.slf4j.LoggerFactory.getLogger(getClass()).debug(\"Language {} does not implement CamelContextAware, skipping auto-configuration properties\", name);\n") .append(" }\n") diff --git a/tooling/camel-spring-boot-generator-maven-plugin/src/test/java/org/apache/camel/springboot/maven/SpringBootAutoConfigurationMojoTest.java b/tooling/camel-spring-boot-generator-maven-plugin/src/test/java/org/apache/camel/springboot/maven/SpringBootAutoConfigurationMojoTest.java new file mode 100644 index 00000000000..e41d3ae1f35 --- /dev/null +++ b/tooling/camel-spring-boot-generator-maven-plugin/src/test/java/org/apache/camel/springboot/maven/SpringBootAutoConfigurationMojoTest.java @@ -0,0 +1,76 @@ +/* + * 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.springboot.maven; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the code that {@link SpringBootAutoConfigurationMojo} generates into the starters. + */ +class SpringBootAutoConfigurationMojoTest { + + @Test + @DisplayName("Generated converters report unresolvable values instead of converting them to null") + void testConvertBodyFailsClosed() { + String body = SpringBootAutoConfigurationMojo.createConvertBody("camel.component.netty-http"); + + assertThat(body).isEqualTo( + "return BeanReferenceHelper.resolveBeanReference(applicationContext, source, targetType," + + " \"camel.component.netty-http\");\n"); + // the old generated body silently returned null for anything that did not start with # + assertThat(body).doesNotContain("return null"); + assertThat(body).doesNotContain("startsWith(\"#\")"); + } + + @Test + @DisplayName("Generated component customizer does not silently drop options") + void testComponentBodyBindsStrictly() { + String body = SpringBootAutoConfigurationMojo.createComponentBody("NettyHttpComponent", "netty-http"); + + assertThat(body).contains("CamelPropertiesHelper.copyConfigurationProperties(target.getCamelContext()," + + " applicationContext,\n \"camel.component.netty-http\"," + + " configuration, target);"); + assertThat(body).doesNotContain("CamelPropertiesHelper.copyProperties("); + assertThat(body).contains("\"camel.component.netty-http.customizer\""); + } + + @Test + @DisplayName("Generated data format customizer does not silently drop options") + void testDataFormatBodyBindsStrictly() { + String body = SpringBootAutoConfigurationMojo.createDataFormatBody("JacksonDataFormat", "jackson"); + + assertThat(body).contains("CamelPropertiesHelper.copyConfigurationProperties(camelContextProvider.getObject()," + + " applicationContext,\n \"camel.dataformat.jackson\"," + + " configuration, target);"); + assertThat(body).doesNotContain("CamelPropertiesHelper.copyProperties("); + } + + @Test + @DisplayName("Generated language customizer does not silently drop options") + void testLanguageBodyBindsStrictly() { + String body = SpringBootAutoConfigurationMojo.createLanguageBody("XPathLanguage", "xpath"); + + assertThat(body).contains("CamelPropertiesHelper.copyConfigurationProperties(cca.getCamelContext()," + + " applicationContext,\n \"camel.language.xpath\"," + + " configuration, target);"); + assertThat(body).doesNotContain("CamelPropertiesHelper.copyProperties("); + } + +}
