You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2024/04/18 13:28:15 UTC

(camel) branch main updated: Pi array (#13848)

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 <cl...@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]