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 d8496319212 Pi array (#13848) d8496319212 is described below commit d849631921216723bd2c068620f7b0e4834f1e2b Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Thu Apr 18 15:28:08 2024 +0200 Pi array (#13848) * CAMEL-20688: Add support for array types for @PropertyInject using a separator * CAMEL-20688: Add support for array types for @PropertyInject using a separator --- .../main/java/org/apache/camel/PropertyInject.java | 6 + .../impl/engine/CamelPostProcessorHelper.java | 82 +++++++++++- .../impl/engine/DefaultCamelBeanPostProcessor.java | 23 ++-- .../impl/engine/CamelPostProcessorHelperTest.java | 143 ++++++++++++++++++++- .../modules/ROOT/pages/bean-integration.adoc | 48 +++++++ 5 files changed, 285 insertions(+), 17 deletions(-) diff --git a/core/camel-api/src/main/java/org/apache/camel/PropertyInject.java b/core/camel-api/src/main/java/org/apache/camel/PropertyInject.java index 387a42fcdf4..8bda8d34c85 100644 --- a/core/camel-api/src/main/java/org/apache/camel/PropertyInject.java +++ b/core/camel-api/src/main/java/org/apache/camel/PropertyInject.java @@ -41,4 +41,10 @@ public @interface PropertyInject { */ String defaultValue() default ""; + /** + * Used for splitting the property value into an array/list of values. For example to use comma to separate the + * values. + */ + String separator() default ""; + } diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelPostProcessorHelper.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelPostProcessorHelper.java index ebced29de92..30b2d3a353e 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelPostProcessorHelper.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/CamelPostProcessorHelper.java @@ -18,7 +18,11 @@ package org.apache.camel.impl.engine; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; import java.util.Properties; @@ -37,6 +41,7 @@ import org.apache.camel.FluentProducerTemplate; import org.apache.camel.IsSingleton; import org.apache.camel.MultipleConsumersSupport; import org.apache.camel.NoSuchBeanTypeException; +import org.apache.camel.NoTypeConversionAvailableException; import org.apache.camel.PollingConsumer; import org.apache.camel.Producer; import org.apache.camel.ProducerTemplate; @@ -54,6 +59,7 @@ 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; @@ -271,7 +277,7 @@ public class CamelPostProcessorHelper implements CamelContextAware { } public Object getInjectionPropertyValue( - Class<?> type, String propertyName, String propertyDefaultValue, + Class<?> type, Type genericType, String propertyName, String propertyDefaultValue, String separator, String injectionPointName, Object bean, String beanName) { try { String key; @@ -286,6 +292,10 @@ public class CamelPostProcessorHelper implements CamelContextAware { } String value = getCamelContext().resolvePropertyPlaceholders(key); if (value != null) { + if (separator != null && !separator.isBlank()) { + Object values = convertValueUsingSeparator(camelContext, type, genericType, value, separator); + return getCamelContext().getTypeConverter().mandatoryConvertTo(type, values); + } return getCamelContext().getTypeConverter().mandatoryConvertTo(type, value); } else { return null; @@ -293,6 +303,11 @@ public class CamelPostProcessorHelper implements CamelContextAware { } catch (Exception e) { if (ObjectHelper.isNotEmpty(propertyDefaultValue)) { try { + if (separator != null && !separator.isBlank()) { + Object values + = convertValueUsingSeparator(camelContext, type, genericType, propertyDefaultValue, separator); + return getCamelContext().getTypeConverter().mandatoryConvertTo(type, values); + } return getCamelContext().getTypeConverter().mandatoryConvertTo(type, propertyDefaultValue); } catch (Exception e2) { throw RuntimeCamelException.wrapRuntimeCamelException(e2); @@ -302,6 +317,65 @@ public class CamelPostProcessorHelper implements CamelContextAware { } } + private static Object convertValueUsingSeparator( + CamelContext camelContext, Class<?> type, Type genericType, + String value, String separator) + throws NoTypeConversionAvailableException { + String[] arr = value.split(separator); + + if (type.isArray()) { + Object[] values = new Object[arr.length]; + Class<?> ct = type.getComponentType(); + for (int i = 0; i < arr.length; i++) { + String v = arr[i].trim(); // trim values as user may have whitespace noise + values[i] = camelContext.getTypeConverter().mandatoryConvertTo(ct, v); + } + return values; + } else if (Collection.class.isAssignableFrom(type)) { + Class<?> ct = Object.class; + if (genericType != null) { + String name = StringHelper.between(genericType.getTypeName(), "<", ">"); + if (name != null) { + Class<?> clazz = camelContext.getClassResolver().resolveClass(name.trim()); + if (clazz != null) { + ct = clazz; + } + } + } + boolean set = type.isAssignableFrom(Set.class); + Collection values = set ? new LinkedHashSet() : new ArrayList(); + for (int i = 0; i < arr.length; i++) { + String v = arr[i].trim(); // trim values as user may have whitespace noise + values.add(camelContext.getTypeConverter().mandatoryConvertTo(ct, v)); + } + return values; + } else if (Map.class.isAssignableFrom(type)) { + Class<?> ct = Object.class; + if (genericType != null) { + String name = StringHelper.between(genericType.getTypeName(), "<", ">"); + name = StringHelper.afterLast(name, ","); + if (name != null) { + Class<?> clazz = camelContext.getClassResolver().resolveClass(name.trim()); + if (clazz != null) { + ct = clazz; + } + } + } + Map<String, Object> values = new LinkedHashMap<>(); + for (int i = 0; i < arr.length; i++) { + String v = arr[i].trim(); // trim values as user may have whitespace noise + if (v.contains("=")) { + String k = StringHelper.before(v, "=").trim(); + String e = StringHelper.after(v, "=").trim(); + values.put(k, camelContext.getTypeConverter().mandatoryConvertTo(ct, e)); + } + } + return values; + } + + return null; + } + public Object getInjectionBeanValue(Class<?> type, String name) { if (ObjectHelper.isEmpty(name)) { // is it camel context itself? @@ -456,6 +530,7 @@ public class CamelPostProcessorHelper implements CamelContextAware { Object[] parameters = new Object[method.getParameterCount()]; for (int i = 0; i < method.getParameterCount(); i++) { Class<?> type = method.getParameterTypes()[i]; + Type genericType = method.getGenericParameterTypes()[i]; if (type.isAssignableFrom(CamelContext.class)) { parameters[i] = context; } else if (type.isAssignableFrom(Registry.class)) { @@ -470,8 +545,9 @@ public class CamelPostProcessorHelper implements CamelContextAware { Annotation ann = anns[0]; if (ann.annotationType() == PropertyInject.class) { PropertyInject pi = (PropertyInject) ann; - Object result = getInjectionPropertyValue(type, pi.value(), pi.defaultValue(), - null, null, null); + Object result + = getInjectionPropertyValue(type, genericType, pi.value(), pi.defaultValue(), pi.separator(), + null, null, null); parameters[i] = result; } else if (ann.annotationType() == BeanConfigInject.class) { BeanConfigInject pi = (BeanConfigInject) ann; diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelBeanPostProcessor.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelBeanPostProcessor.java index fc0d5cf86d2..45d6e1e1011 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelBeanPostProcessor.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelBeanPostProcessor.java @@ -18,6 +18,7 @@ package org.apache.camel.impl.engine; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -268,7 +269,8 @@ public class DefaultCamelBeanPostProcessor implements CamelBeanPostProcessor, Ca PropertyInject propertyInject = field.getAnnotation(PropertyInject.class); if (propertyInject != null) { - injectFieldProperty(field, propertyInject.value(), propertyInject.defaultValue(), bean, beanName); + injectFieldProperty(field, propertyInject.value(), propertyInject.defaultValue(), propertyInject.separator(), + bean, beanName); } BeanInject beanInject = field.getAnnotation(BeanInject.class); @@ -326,10 +328,12 @@ public class DefaultCamelBeanPostProcessor implements CamelBeanPostProcessor, Ca } public void injectFieldProperty( - Field field, String propertyName, String propertyDefaultValue, Object bean, String beanName) { + Field field, String propertyName, String propertyDefaultValue, String propertySeparator, + Object bean, String beanName) { ReflectionHelper.setField(field, bean, - getPostProcessorHelper().getInjectionPropertyValue(field.getType(), propertyName, propertyDefaultValue, - field.getName(), bean, beanName)); + getPostProcessorHelper().getInjectionPropertyValue(field.getType(), field.getGenericType(), propertyName, + propertyDefaultValue, + propertySeparator, field.getName(), bean, beanName)); } protected void injectMethods(final Object bean, final String beanName, Function<Class<?>, Boolean> accept) { @@ -424,7 +428,8 @@ public class DefaultCamelBeanPostProcessor implements CamelBeanPostProcessor, Ca protected void setterInjection(Method method, Object bean, String beanName) { PropertyInject propertyInject = method.getAnnotation(PropertyInject.class); if (propertyInject != null) { - setterPropertyInjection(method, propertyInject.value(), propertyInject.defaultValue(), bean, beanName); + setterPropertyInjection(method, propertyInject.value(), propertyInject.defaultValue(), propertyInject.separator(), + bean, beanName); } BeanInject beanInject = method.getAnnotation(BeanInject.class); @@ -461,15 +466,17 @@ public class DefaultCamelBeanPostProcessor implements CamelBeanPostProcessor, Ca } public void setterPropertyInjection( - Method method, String propertyValue, String propertyDefaultValue, + Method method, String propertyValue, String propertyDefaultValue, String propertySeparator, Object bean, String beanName) { Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length != 1) { LOG.warn("Ignoring badly annotated method for injection due to incorrect number of parameters: {}", method); } else { String propertyName = org.apache.camel.util.ObjectHelper.getPropertyName(method); - Object value = getPostProcessorHelper().getInjectionPropertyValue(parameterTypes[0], propertyValue, - propertyDefaultValue, propertyName, bean, beanName); + Class<?> type = parameterTypes[0]; + Type genericType = method.getGenericParameterTypes()[0]; + Object value = getPostProcessorHelper().getInjectionPropertyValue(type, genericType, propertyValue, + propertyDefaultValue, propertySeparator, propertyName, bean, beanName); invokeMethod(method, bean, value); } } diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/CamelPostProcessorHelperTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/CamelPostProcessorHelperTest.java index 212a7e868f9..28f4c8fed29 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/engine/CamelPostProcessorHelperTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/CamelPostProcessorHelperTest.java @@ -18,7 +18,11 @@ package org.apache.camel.impl.engine; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.camel.BeanConfigInject; @@ -374,13 +378,13 @@ public class CamelPostProcessorHelperTest extends ContextTestSupport { Field field = bean.getClass().getField("timeout"); PropertyInject propertyInject = field.getAnnotation(PropertyInject.class); Class<?> type = field.getType(); - Object value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "timeout", bean, "foo"); + Object value = helper.getInjectionPropertyValue(type, null, propertyInject.value(), "", "", "timeout", bean, "foo"); assertEquals(Integer.valueOf(2000), (Object) Integer.valueOf(String.valueOf(value))); field = bean.getClass().getField("greeting"); propertyInject = field.getAnnotation(PropertyInject.class); type = field.getType(); - value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "greeting", bean, "foo"); + value = helper.getInjectionPropertyValue(type, null, propertyInject.value(), "", "", "greeting", bean, "foo"); assertEquals("Hello Camel", value); } @@ -395,16 +399,106 @@ public class CamelPostProcessorHelperTest extends ContextTestSupport { Field field = bean.getClass().getField("timeout"); PropertyInject propertyInject = field.getAnnotation(PropertyInject.class); Class<?> type = field.getType(); - Object value = helper.getInjectionPropertyValue(type, propertyInject.value(), "5000", "timeout", bean, "foo"); + Object value = helper.getInjectionPropertyValue(type, null, propertyInject.value(), "5000", "", "timeout", bean, "foo"); assertEquals(Integer.valueOf(5000), (Object) Integer.valueOf(String.valueOf(value))); field = bean.getClass().getField("greeting"); propertyInject = field.getAnnotation(PropertyInject.class); type = field.getType(); - value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "greeting", bean, "foo"); + value = helper.getInjectionPropertyValue(type, null, propertyInject.value(), "", "", "greeting", bean, "foo"); assertEquals("Hello Camel", value); } + @Test + public void testPropertyFieldSeparatorArrayInject() throws Exception { + myProp.put("serverPorts", "4444;5555"); // test with semicolon as separator + myProp.put("hosts", "serverA , serverB"); // test with whitespace noise + + CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context); + + MyPropertyFieldSeparatorArrayBean bean = new MyPropertyFieldSeparatorArrayBean(); + + Field field = bean.getClass().getField("ports"); + PropertyInject propertyInject = field.getAnnotation(PropertyInject.class); + Class<?> type = field.getType(); + Object value + = helper.getInjectionPropertyValue(type, null, propertyInject.value(), "", propertyInject.separator(), "ports", + bean, "foo"); + assertIsInstanceOf(int[].class, value); + int[] arr = (int[]) value; + assertEquals(2, arr.length); + assertEquals(4444, arr[0]); + assertEquals(5555, arr[1]); + + field = bean.getClass().getField("hosts"); + propertyInject = field.getAnnotation(PropertyInject.class); + type = field.getType(); + value = helper.getInjectionPropertyValue(type, null, propertyInject.value(), "", propertyInject.separator(), "hosts", + bean, + "foo"); + assertIsInstanceOf(String[].class, value); + String[] arr2 = (String[]) value; + assertEquals(2, arr2.length); + assertEquals("serverA", arr2[0]); + assertEquals("serverB", arr2[1]); + } + + @Test + public void testPropertyFieldSeparatorListInject() throws Exception { + myProp.put("serverPorts", "4444;5555"); // test with semicolon as separator + myProp.put("hosts", "serverA , serverB"); // test with whitespace noise + + CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context); + + MyPropertyFieldSeparatorListBean bean = new MyPropertyFieldSeparatorListBean(); + + Field field = bean.getClass().getField("ports"); + PropertyInject propertyInject = field.getAnnotation(PropertyInject.class); + Class<?> type = field.getType(); + Object value = helper.getInjectionPropertyValue(type, field.getGenericType(), + propertyInject.value(), "", propertyInject.separator(), "ports", + bean, "foo"); + assertIsInstanceOf(List.class, value); + List arr = (List) value; + assertEquals(2, arr.size()); + assertEquals(4444, arr.get(0)); + assertEquals(5555, arr.get(1)); + + field = bean.getClass().getField("hosts"); + propertyInject = field.getAnnotation(PropertyInject.class); + type = field.getType(); + value = helper.getInjectionPropertyValue(type, field.getGenericType(), + propertyInject.value(), "", propertyInject.separator(), "hosts", bean, + "foo"); + assertIsInstanceOf(Set.class, value); + Set arr2 = (Set) value; + assertEquals(2, arr.size()); + Iterator it = arr2.iterator(); + assertEquals("serverA", it.next()); + assertEquals("serverB", it.next()); + } + + @Test + public void testPropertyFieldSeparatorMapInject() throws Exception { + myProp.put("servers", "serverA = 4444 ; serverB=5555"); // test with semicolon as separator and whitespace + + CamelPostProcessorHelper helper = new CamelPostProcessorHelper(context); + + MyPropertyFieldSeparatorMapBean bean = new MyPropertyFieldSeparatorMapBean(); + + Field field = bean.getClass().getField("servers"); + PropertyInject propertyInject = field.getAnnotation(PropertyInject.class); + Class<?> type = field.getType(); + Object value = helper.getInjectionPropertyValue(type, field.getGenericType(), + propertyInject.value(), "", propertyInject.separator(), "servers", + bean, "foo"); + assertIsInstanceOf(Map.class, value); + Map arr = (Map) value; + assertEquals(2, arr.size()); + assertEquals(4444, arr.get("serverA")); + assertEquals(5555, arr.get("serverB")); + } + @Test public void testPropertyMethodInject() throws Exception { myProp.put("myTimeout", "2000"); @@ -417,13 +511,13 @@ public class CamelPostProcessorHelperTest extends ContextTestSupport { Method method = bean.getClass().getMethod("setTimeout", int.class); PropertyInject propertyInject = method.getAnnotation(PropertyInject.class); Class<?> type = method.getParameterTypes()[0]; - Object value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "timeout", bean, "foo"); + Object value = helper.getInjectionPropertyValue(type, null, propertyInject.value(), "", "", "timeout", bean, "foo"); assertEquals(Integer.valueOf(2000), (Object) Integer.valueOf(String.valueOf(value))); method = bean.getClass().getMethod("setGreeting", String.class); propertyInject = method.getAnnotation(PropertyInject.class); type = method.getParameterTypes()[0]; - value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", "greeting", bean, "foo"); + value = helper.getInjectionPropertyValue(type, null, propertyInject.value(), "", "", "greeting", bean, "foo"); assertEquals("Hello Camel", value); } @@ -750,6 +844,43 @@ public class CamelPostProcessorHelperTest extends ContextTestSupport { } } + public static class MyPropertyFieldSeparatorArrayBean { + + @PropertyInject(value = "serverPorts", separator = ";") + public int[] ports; + + @PropertyInject(value = "hosts", separator = ",") + public String[] hosts; + + public String doSomething(String body) { + return String.format("%s:%d %s:%d with body: %s", hosts[0], ports[0], hosts[1], ports[1], body); + } + } + + public static class MyPropertyFieldSeparatorListBean { + + @PropertyInject(value = "serverPorts", separator = ";") + public List<Integer> ports; + + @PropertyInject(value = "hosts", separator = ",") + public Set<String> hosts; + + public String doSomething(String body) { + Iterator<String> it = hosts.iterator(); + return String.format("%s:%d %s:%d with body: %s", it.next(), ports.get(0), it.next(), ports.get(1), body); + } + } + + public static class MyPropertyFieldSeparatorMapBean { + + @PropertyInject(value = "servers", separator = ";") + public Map<String, Integer> servers; + + public String doSomething(String body) { + return null; + } + } + public static class MyPropertyMethodBean { private int timeout; diff --git a/docs/user-manual/modules/ROOT/pages/bean-integration.adoc b/docs/user-manual/modules/ROOT/pages/bean-integration.adoc index 2a047836918..e3036f18278 100644 --- a/docs/user-manual/modules/ROOT/pages/bean-integration.adoc +++ b/docs/user-manual/modules/ROOT/pages/bean-integration.adoc @@ -92,6 +92,54 @@ You can also add a default value if the key does not exist, such as: private int timeout; ---- +=== Using @PropertyInject with arrays, lists, sets or maps + +You can also use `@PropertyInject` to inject an array of values. For example, you may configure multiple hostnames +in the configuration file, and need to inject this into an `String[]` or `List<String>` field. +To do this you need to tell Camel that the property value should be split using a separator, as follows: + +[source,java] +---- +@PropertyInject(value = "myHostnames", separator = ",") +private String[] servers; +---- + +TIP: You can also use list/set types, such as `List<String>` or `Set<String>` instead of array. + +Then in the `application.properties` file you can define the servers: + +[source,properties] +---- +myHostnames = serverA, serverB, serverC +---- + +TIP: This also works for fields that are not String based, such as `int[]` for numeric values. + +For `Map` types then the values is expected to be in key=value format, such as: + +[source,properties] +---- +myServers = serverA=http://coolstore:4444,serverB=http://megastore:5555 +---- + +You can then inject this into a `Map` as follows: + +[source,java] +---- +@PropertyInject(value = "myServers", separator = ",") +private Map servers; +---- + +You can use generic types in the Map such as the values should be `Integer` values: + +[source,java] +---- +@PropertyInject(value = "ports", separator = ",") +private Map<String, Integer> ports; +---- + +NOTE: The generic type can only be a single class type, and cannot be a nested complex type such as `Map<String,Map<Kind,Priority>>`. + == See Also ** xref:bean-injection.adoc[Bean Injection]