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 2019/11/11 18:33:40 UTC

[camel] 01/02: CAMEL-14153: Add support for constructor parameters to property binding support - eg for camel-main configurations.

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

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

commit dad2e1f705acc816b933cb4c808403df9bdab575
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Mon Nov 11 19:19:26 2019 +0100

    CAMEL-14153: Add support for constructor parameters to property binding support - eg for camel-main configurations.
---
 .../test/java/org/apache/camel/support/Animal.java |  50 ++++
 .../camel/support/PropertyBindingSupportTest.java  |  34 +++
 .../camel/support/PropertyBindingSupport.java      | 258 +++++++++++++++++----
 3 files changed, 293 insertions(+), 49 deletions(-)

diff --git a/core/camel-core/src/test/java/org/apache/camel/support/Animal.java b/core/camel-core/src/test/java/org/apache/camel/support/Animal.java
new file mode 100644
index 0000000..73ada4b
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/support/Animal.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.support;
+
+public class Animal {
+    private String name;
+    private boolean dangerous;
+
+    public Animal() {
+    }
+
+    public Animal(String name) {
+        this.name = name;
+    }
+
+    public Animal(boolean dangerous, int foo) {
+        throw new IllegalArgumentException("Should not be invoked");
+    }
+
+    public Animal(String name, boolean dangerous) {
+        this.name = name;
+        this.dangerous = dangerous;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public boolean isDangerous() {
+        return dangerous;
+    }
+
+    public void setDangerous(boolean dangerous) {
+        this.dangerous = dangerous;
+    }
+}
diff --git a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportTest.java b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportTest.java
index eb995d7..f0b2bec 100644
--- a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportTest.java
+++ b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportTest.java
@@ -346,9 +346,35 @@ public class PropertyBindingSupportTest extends ContextTestSupport {
         }
     }
 
+    @Test
+    public void testNestedClassConstructorParameterOneParameter() throws Exception {
+        Foo foo = new Foo();
+
+        PropertyBindingSupport.build().bind(context, foo, "name", "James");
+        PropertyBindingSupport.build().bind(context, foo, "animal", "#class:org.apache.camel.support.Animal('Tony Tiger')");
+        PropertyBindingSupport.build().bind(context, foo, "animal.dangerous", "true");
+
+        assertEquals("James", foo.getName());
+        assertEquals("Tony Tiger", foo.getAnimal().getName());
+        assertEquals(true, foo.getAnimal().isDangerous());
+    }
+
+    @Test
+    public void testNestedClassConstructorParameterTwoParameter() throws Exception {
+        Foo foo = new Foo();
+
+        PropertyBindingSupport.build().bind(context, foo, "name", "James");
+        PropertyBindingSupport.build().bind(context, foo, "animal", "#class:org.apache.camel.support.Animal('Donald Duck', false)");
+
+        assertEquals("James", foo.getName());
+        assertEquals("Donald Duck", foo.getAnimal().getName());
+        assertEquals(false, foo.getAnimal().isDangerous());
+    }
+
     public static class Foo {
         private String name;
         private Bar bar = new Bar();
+        private Animal animal;
 
         public String getName() {
             return name;
@@ -365,6 +391,14 @@ public class PropertyBindingSupportTest extends ContextTestSupport {
         public void setBar(Bar bar) {
             this.bar = bar;
         }
+
+        public Animal getAnimal() {
+            return animal;
+        }
+
+        public void setAnimal(Animal animal) {
+            this.animal = animal;
+        }
     }
 
     public static class Bar {
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java b/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java
index f88621e..ce63ff1 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java
@@ -16,7 +16,9 @@
  */
 package org.apache.camel.support;
 
+import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -32,6 +34,7 @@ import org.apache.camel.PropertyBindingException;
 import org.apache.camel.spi.GeneratedPropertyConfigurer;
 import org.apache.camel.spi.PropertyConfigurer;
 import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.StringQuoteHelper;
 
 import static org.apache.camel.util.ObjectHelper.isNotEmpty;
 
@@ -49,7 +52,9 @@ import static org.apache.camel.util.ObjectHelper.isNotEmpty;
  *     <li>autowire by type - Values can refer to singleton beans by auto wiring by setting the value to #autowired</li>
  *     <li>reference new class - Values can refer to creating new beans by their class name by prefixing with #class, eg #class:com.foo.MyClassType.
  *                               The class is created using a default no-arg constructor, however if you need to create the instance via a factory method
- *                               then you specify the method as shown: #class:com.foo.MyClassType#myFactoryMethod</li>.
+ *                               then you specify the method as shown: #class:com.foo.MyClassType#myFactoryMethod.
+ *                               Or if you need to create the instance via constructor parameters then you can specify the parameters as shown:
+ *                               #class:com.foo.MyClass('Hello World', 5, true)</li>.
  *     <li>ignore case - Whether to ignore case for property keys<li>
  * </ul>
  */
@@ -211,7 +216,7 @@ public final class PropertyBindingSupport {
         /**
          * Binds the properties to the target object, and removes the property that was bound from properties.
          *
-         * @return  true if one or more properties was bound
+         * @return true if one or more properties was bound
          */
         public boolean bind() {
             // mandatory parameters
@@ -226,10 +231,10 @@ public final class PropertyBindingSupport {
         /**
          * Binds the properties to the target object, and removes the property that was bound from properties.
          *
-         * @param camelContext  the camel context
-         * @param target        the target object
-         * @param properties    the properties where the bound properties will be removed from
-         * @return              true if one or more properties was bound
+         * @param camelContext the camel context
+         * @param target       the target object
+         * @param properties   the properties where the bound properties will be removed from
+         * @return true if one or more properties was bound
          */
         public boolean bind(CamelContext camelContext, Object target, Map<String, Object> properties) {
             CamelContext context = camelContext != null ? camelContext : this.camelContext;
@@ -248,11 +253,11 @@ public final class PropertyBindingSupport {
         /**
          * Binds the property to the target object.
          *
-         * @param camelContext  the camel context
-         * @param target        the target object
-         * @param key           the property key
-         * @param value         the property value
-         * @return              true if the property was bound
+         * @param camelContext the camel context
+         * @param target       the target object
+         * @param key          the property key
+         * @param value        the property value
+         * @return true if the property was bound
          */
         public boolean bind(CamelContext camelContext, Object target, String key, Object value) {
             org.apache.camel.util.ObjectHelper.notNull(camelContext, "camelContext");
@@ -282,10 +287,10 @@ public final class PropertyBindingSupport {
         /**
          * Callback when a property was autowired on a bean
          *
-         * @param target        the targeted bean
-         * @param propertyName  the name of the property
-         * @param propertyType  the type of the property
-         * @param value         the property value
+         * @param target       the targeted bean
+         * @param propertyName the name of the property
+         * @param propertyType the type of the property
+         * @param value        the property value
          */
         void onAutowire(Object target, String propertyName, Class propertyType, Object value);
 
@@ -297,9 +302,9 @@ public final class PropertyBindingSupport {
      * This is used for convention over configuration to automatic configure resources such as DataSource, Amazon Logins and
      * so on.
      *
-     * @param camelContext  the camel context
-     * @param target        the target object
-     * @return              true if one ore more properties was auto wired
+     * @param camelContext the camel context
+     * @param target       the target object
+     * @return true if one ore more properties was auto wired
      */
     public static boolean autowireSingletonPropertiesFromRegistry(CamelContext camelContext, Object target) {
         return autowireSingletonPropertiesFromRegistry(camelContext, target, false, false, null);
@@ -311,14 +316,14 @@ public final class PropertyBindingSupport {
      * This is used for convention over configuration to automatic configure resources such as DataSource, Amazon Logins and
      * so on.
      *
-     * @param camelContext  the camel context
-     * @param target        the target object
-     * @param bindNullOnly  whether to only autowire if the property has no default value or has not been configured explicit
-     * @param deepNesting   whether to attempt to walk as deep down the object graph by creating new empty objects on the way if needed (Camel can only create
-     *                      new empty objects if they have a default no-arg constructor, also mind that this may lead to creating many empty objects, even
-     *                      if they will not have any objects autowired from the registry, so use this with caution)
-     * @param callback      optional callback when a property was auto wired
-     * @return              true if one ore more properties was auto wired
+     * @param camelContext the camel context
+     * @param target       the target object
+     * @param bindNullOnly whether to only autowire if the property has no default value or has not been configured explicit
+     * @param deepNesting  whether to attempt to walk as deep down the object graph by creating new empty objects on the way if needed (Camel can only create
+     *                     new empty objects if they have a default no-arg constructor, also mind that this may lead to creating many empty objects, even
+     *                     if they will not have any objects autowired from the registry, so use this with caution)
+     * @param callback     optional callback when a property was auto wired
+     * @return true if one ore more properties was auto wired
      */
     public static boolean autowireSingletonPropertiesFromRegistry(CamelContext camelContext, Object target,
                                                                   boolean bindNullOnly, boolean deepNesting, OnAutowiring callback) {
@@ -422,11 +427,10 @@ public final class PropertyBindingSupport {
      * the fluent builder {@link #build()} where each option can be customized, such as whether parameter
      * should be removed, or whether options are mandatory etc.
      *
-     * @param camelContext  the camel context
-     * @param target        the target object
-     * @param properties    the properties where the bound properties will be removed from
-     * @return              true if one or more properties was bound
-     *
+     * @param camelContext the camel context
+     * @param target       the target object
+     * @param properties   the properties where the bound properties will be removed from
+     * @return true if one or more properties was bound
      * @see #build()
      */
     public static boolean bindProperties(CamelContext camelContext, Object target, Map<String, Object> properties) {
@@ -442,22 +446,22 @@ public final class PropertyBindingSupport {
      * Binds the properties with the given prefix to the target object, and removes the property that was bound from properties.
      * Note that the prefix is removed from the key before the property is bound.
      *
-     * @param camelContext        the camel context
-     * @param target              the target object
-     * @param properties          the properties where the bound properties will be removed from
-     * @param optionPrefix        the prefix used to filter properties
-     * @param ignoreCase          whether to ignore case for property keys
-     * @param removeParameter     whether to remove bound parameters
-     * @param mandatory           whether all parameters must be bound
-     * @param nesting             whether nesting is in use
-     * @param deepNesting         whether deep nesting is in use, where Camel will attempt to walk as deep as possible by creating new objects in the OGNL graph if
-     *                            a property has a setter and the object can be created from a default no-arg constructor.
-     * @param fluentBuilder       whether fluent builder is allowed as a valid getter/setter
-     * @param allowPrivateSetter  whether autowiring components allows to use private setter method when setting the value
-     * @param reference           whether reference parameter (syntax starts with #) is in use
-     * @param placeholder         whether to use Camels property placeholder to resolve placeholders on keys and values
-     * @param configurer          to use an optional {@link org.apache.camel.spi.PropertyConfigurer} to configure the properties
-     * @return                    true if one or more properties was bound
+     * @param camelContext       the camel context
+     * @param target             the target object
+     * @param properties         the properties where the bound properties will be removed from
+     * @param optionPrefix       the prefix used to filter properties
+     * @param ignoreCase         whether to ignore case for property keys
+     * @param removeParameter    whether to remove bound parameters
+     * @param mandatory          whether all parameters must be bound
+     * @param nesting            whether nesting is in use
+     * @param deepNesting        whether deep nesting is in use, where Camel will attempt to walk as deep as possible by creating new objects in the OGNL graph if
+     *                           a property has a setter and the object can be created from a default no-arg constructor.
+     * @param fluentBuilder      whether fluent builder is allowed as a valid getter/setter
+     * @param allowPrivateSetter whether autowiring components allows to use private setter method when setting the value
+     * @param reference          whether reference parameter (syntax starts with #) is in use
+     * @param placeholder        whether to use Camels property placeholder to resolve placeholders on keys and values
+     * @param configurer         to use an optional {@link org.apache.camel.spi.PropertyConfigurer} to configure the properties
+     * @return true if one or more properties was bound
      */
     private static boolean doBindProperties(CamelContext camelContext, Object target, Map<String, Object> properties,
                                             String optionPrefix, boolean ignoreCase, boolean removeParameter, boolean mandatory,
@@ -486,7 +490,7 @@ public final class PropertyBindingSupport {
                     // not resolve property placeholders eventually defined in the value before invoking
                     // the setter.
                     if (value instanceof String) {
-                        value = camelContext.resolvePropertyPlaceholders((String)value);
+                        value = camelContext.resolvePropertyPlaceholders((String) value);
                     }
                     try {
                         value = resolveValue(camelContext, target, key, value, ignoreCase, fluentBuilder, allowPrivateSetter);
@@ -559,13 +563,22 @@ public final class PropertyBindingSupport {
                 // its a new class to be created
                 String className = value.toString().substring(7);
                 String factoryMethod = null;
-                if (className.indexOf('#') != -1) {
+                String parameters = null;
+                if (className.endsWith(")") && className.indexOf('(') != -1) {
+                    parameters = StringHelper.after(className, "(");
+                    parameters = parameters.substring(0, parameters.length() - 1); // clip last )
+                    className = StringHelper.before(className, "(");
+                }
+                if (className != null && className.indexOf('#') != -1) {
                     factoryMethod = StringHelper.after(className, "#");
                     className = StringHelper.before(className, "#");
                 }
                 Class<?> type = context.getClassResolver().resolveMandatoryClass(className);
                 if (factoryMethod != null) {
                     value = context.getInjector().newInstance(type, factoryMethod);
+                } else if (parameters != null) {
+                    // special to support constructor parameters
+                    value = newInstanceConstructorParameters(context, type, parameters);
                 } else {
                     value = context.getInjector().newInstance(type);
                 }
@@ -800,5 +813,152 @@ public final class PropertyBindingSupport {
         return parameter != null && parameter.trim().startsWith("#");
     }
 
+    private static Object newInstanceConstructorParameters(CamelContext camelContext, Class<?> type, String parameters) throws Exception {
+        String[] params = StringQuoteHelper.splitSafeQuote(parameters, ',');
+        Constructor found = findMatchingConstructor(type.getConstructors(), params);
+        if (found != null) {
+            Object[] arr = new Object[found.getParameterCount()];
+            for (int i = 0; i < found.getParameterCount(); i++) {
+                Class<?> paramType = found.getParameterTypes()[i];
+                Object param = params[i];
+                Object val = camelContext.getTypeConverter().convertTo(paramType, param);
+                // unquote text
+                if (val instanceof String) {
+                    val = StringHelper.removeLeadingAndEndingQuotes((String) val);
+                }
+                arr[i] = val;
+            }
+            return found.newInstance(arr);
+        }
+        return null;
+    }
+
+    /**
+     * Finds the best matching constructor for the given parameters.
+     * <p/>
+     * This implementation is similar to the logic in camel-bean.
+     *
+     * @param constructors the constructors
+     * @param params       the parameters
+     * @return the constructor, or null if no matching constructor can be found
+     */
+    private static Constructor findMatchingConstructor(Constructor<?>[] constructors, String[] params) {
+        List<Constructor> candidates = new ArrayList<>();
+        Constructor fallbackCandidate = null;
+
+        for (Constructor ctr : constructors) {
+            if (ctr.getParameterCount() != params.length) {
+                continue;
+            }
+
+            boolean matches = true;
+            for (int i = 0; i < ctr.getParameterCount(); i++) {
+                String parameter = params[i];
+                if (parameter != null) {
+                    // must trim
+                    parameter = parameter.trim();
+                }
+
+                Class<?> parameterType = getValidParameterType(parameter);
+                Class<?> expectedType = ctr.getParameterTypes()[i];
+
+                if (parameterType != null && expectedType != null) {
+                    // skip java.lang.Object type, when we have multiple possible methods we want to avoid it if possible
+                    if (Object.class.equals(expectedType)) {
+                        fallbackCandidate = ctr;
+                        matches = false;
+                        break;
+                    }
+
+                    boolean matchingTypes = isParameterMatchingType(parameterType, expectedType);
+                    if (!matchingTypes) {
+                        matches = false;
+                        break;
+                    }
+                }
+            }
+
+            if (matches) {
+                candidates.add(ctr);
+            }
+        }
+
+        return candidates.size() == 1 ? candidates.get(0) : fallbackCandidate;
+    }
+
+    /**
+     * Determines and maps the given value is valid according to the supported
+     * values by the bean component.
+     * <p/>
+     * This implementation is similar to the logic in camel-bean.
+     *
+     * @param value the value
+     * @return the parameter type the given value is being mapped as, or <tt>null</tt> if not valid.
+     */
+    private static Class<?> getValidParameterType(String value) {
+        if (org.apache.camel.util.ObjectHelper.isEmpty(value)) {
+            return null;
+        }
+
+        // trim value
+        value = value.trim();
+
+        // single quoted is valid
+        if (value.startsWith("'") && value.endsWith("'")) {
+            return String.class;
+        }
+
+        // double quoted is valid
+        if (value.startsWith("\"") && value.endsWith("\"")) {
+            return String.class;
+        }
+
+        // true or false is valid (boolean)
+        if (value.equals("true") || value.equals("false")) {
+            return Boolean.class;
+        }
+
+        // null is valid (to force a null value)
+        if (value.equals("null")) {
+            return Object.class;
+        }
+
+        // simple language tokens is valid
+        if (StringHelper.hasStartToken(value, "simple")) {
+            return Object.class;
+        }
+
+        // numeric is valid
+        boolean numeric = true;
+        for (char ch : value.toCharArray()) {
+            if (!Character.isDigit(ch)) {
+                numeric = false;
+                break;
+            }
+        }
+        if (numeric) {
+            return Number.class;
+        }
+
+        // not valid
+        return null;
+    }
+
+    private static boolean isParameterMatchingType(Class<?> parameterType, Class<?> expectedType) {
+        if (Number.class.equals(parameterType)) {
+            // number should match long/int/etc.
+            if (Integer.class.isAssignableFrom(expectedType) || Long.class.isAssignableFrom(expectedType)
+                    || int.class.isAssignableFrom(expectedType) || long.class.isAssignableFrom(expectedType)) {
+                return true;
+            }
+        }
+        if (Boolean.class.equals(parameterType)) {
+            // boolean should match both Boolean and boolean
+            if (Boolean.class.isAssignableFrom(expectedType) || boolean.class.isAssignableFrom(expectedType)) {
+                return true;
+            }
+        }
+        return parameterType.isAssignableFrom(expectedType);
+    }
 
 }