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:09:45 UTC

(camel) 02/02: CAMEL-20688: Add support for array types for @PropertyInject using a separator

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

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

commit 23293673eb8a1abb671f20885ae511708ca9174a
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Apr 18 15:08:19 2024 +0200

    CAMEL-20688: Add support for array types for @PropertyInject using a separator
---
 .../impl/engine/CamelPostProcessorHelper.java      |  69 +++++++++++--
 .../impl/engine/DefaultCamelBeanPostProcessor.java |   8 +-
 .../impl/engine/CamelPostProcessorHelperTest.java  | 110 ++++++++++++++++++---
 .../modules/ROOT/pages/bean-integration.adoc       |  31 +++++-
 4 files changed, 195 insertions(+), 23 deletions(-)

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 a5076a12f86..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;
@@ -55,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;
 
@@ -272,7 +277,7 @@ public class CamelPostProcessorHelper implements CamelContextAware {
     }
 
     public Object getInjectionPropertyValue(
-            Class<?> type, String propertyName, String propertyDefaultValue, String separator,
+            Class<?> type, Type genericType, String propertyName, String propertyDefaultValue, String separator,
             String injectionPointName, Object bean, String beanName) {
         try {
             String key;
@@ -288,7 +293,7 @@ public class CamelPostProcessorHelper implements CamelContextAware {
             String value = getCamelContext().resolvePropertyPlaceholders(key);
             if (value != null) {
                 if (separator != null && !separator.isBlank()) {
-                    Object[] values = convertValueUsingSeparator(camelContext, type, value, separator);
+                    Object values = convertValueUsingSeparator(camelContext, type, genericType, value, separator);
                     return getCamelContext().getTypeConverter().mandatoryConvertTo(type, values);
                 }
                 return getCamelContext().getTypeConverter().mandatoryConvertTo(type, value);
@@ -299,7 +304,8 @@ public class CamelPostProcessorHelper implements CamelContextAware {
             if (ObjectHelper.isNotEmpty(propertyDefaultValue)) {
                 try {
                     if (separator != null && !separator.isBlank()) {
-                        Object[] values = convertValueUsingSeparator(camelContext, type, propertyDefaultValue, separator);
+                        Object values
+                                = convertValueUsingSeparator(camelContext, type, genericType, propertyDefaultValue, separator);
                         return getCamelContext().getTypeConverter().mandatoryConvertTo(type, values);
                     }
                     return getCamelContext().getTypeConverter().mandatoryConvertTo(type, propertyDefaultValue);
@@ -311,18 +317,63 @@ public class CamelPostProcessorHelper implements CamelContextAware {
         }
     }
 
-    private static Object[] convertValueUsingSeparator(CamelContext camelContext, Class<?> type, String value, String separator)
+    private static Object convertValueUsingSeparator(
+            CamelContext camelContext, Class<?> type, Type genericType,
+            String value, String separator)
             throws NoTypeConversionAvailableException {
         String[] arr = value.split(separator);
-        Object[] values = new Object[arr.length];
+
         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 values;
+
+        return null;
     }
 
     public Object getInjectionBeanValue(Class<?> type, String name) {
@@ -479,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)) {
@@ -493,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(), pi.separator(),
-                                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 d9a815eda59..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;
@@ -330,7 +331,8 @@ public class DefaultCamelBeanPostProcessor implements CamelBeanPostProcessor, Ca
             Field field, String propertyName, String propertyDefaultValue, String propertySeparator,
             Object bean, String beanName) {
         ReflectionHelper.setField(field, bean,
-                getPostProcessorHelper().getInjectionPropertyValue(field.getType(), propertyName, propertyDefaultValue,
+                getPostProcessorHelper().getInjectionPropertyValue(field.getType(), field.getGenericType(), propertyName,
+                        propertyDefaultValue,
                         propertySeparator, field.getName(), bean, beanName));
     }
 
@@ -471,7 +473,9 @@ public class DefaultCamelBeanPostProcessor implements CamelBeanPostProcessor, Ca
             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,
+            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 660ea19d4fd..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,30 +399,31 @@ 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 testPropertyFieldSeparatorInject() throws Exception {
+    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);
 
-        MyPropertyFieldSeparatorBean bean = new MyPropertyFieldSeparatorBean();
+        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, propertyInject.value(), "", propertyInject.separator(), "ports",
-                bean, "foo");
+        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);
@@ -428,7 +433,8 @@ public class CamelPostProcessorHelperTest extends ContextTestSupport {
         field = bean.getClass().getField("hosts");
         propertyInject = field.getAnnotation(PropertyInject.class);
         type = field.getType();
-        value = helper.getInjectionPropertyValue(type, propertyInject.value(), "", propertyInject.separator(), "hosts", bean,
+        value = helper.getInjectionPropertyValue(type, null, propertyInject.value(), "", propertyInject.separator(), "hosts",
+                bean,
                 "foo");
         assertIsInstanceOf(String[].class, value);
         String[] arr2 = (String[]) value;
@@ -437,6 +443,62 @@ public class CamelPostProcessorHelperTest extends ContextTestSupport {
         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");
@@ -449,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);
     }
 
@@ -782,7 +844,7 @@ public class CamelPostProcessorHelperTest extends ContextTestSupport {
         }
     }
 
-    public static class MyPropertyFieldSeparatorBean {
+    public static class MyPropertyFieldSeparatorArrayBean {
 
         @PropertyInject(value = "serverPorts", separator = ";")
         public int[] ports;
@@ -795,6 +857,30 @@ public class CamelPostProcessorHelperTest extends ContextTestSupport {
         }
     }
 
+    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 7e5911a0902..e3036f18278 100644
--- a/docs/user-manual/modules/ROOT/pages/bean-integration.adoc
+++ b/docs/user-manual/modules/ROOT/pages/bean-integration.adoc
@@ -92,7 +92,9 @@ You can also add a default value if the key does not exist, such as:
 private int timeout;
 ----
 
-You can also use `@PropertyInject` to inject an array of values. For example you may configure multiple hostnames
+=== 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:
 
@@ -102,6 +104,8 @@ To do this you need to tell Camel that the property value should be split using
 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]
@@ -111,6 +115,31 @@ 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]