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/06/17 12:04:36 UTC

[camel] 01/20: CAMEL-13647: Allow to do autowrire by classpath. Quick and dirty prototype.

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 d473ed4e5940cc6d43a4ec51fa15f3c48e311e5f
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Fri Jun 14 11:45:19 2019 +0200

    CAMEL-13647: Allow to do autowrire by classpath. Quick and dirty prototype.
---
 .../org/apache/camel/spi/PackageScanFilter.java    |   1 +
 .../impl/scan/AssignableToPackageScanFilter.java   |  14 +-
 .../apache/camel/support/MyBarImplementation.java} |  44 +++--
 .../org/apache/camel/support/MyBarInterface.java}  |  30 ++-
 ...ropertyBindingSupportAutowireClasspathTest.java |  64 ++++++
 .../camel/support/PropertyBindingSupport.java      | 219 ++++++++++++++++++++-
 6 files changed, 337 insertions(+), 35 deletions(-)

diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java b/core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java
index d87f1be..6129faf 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java
@@ -19,6 +19,7 @@ package org.apache.camel.spi;
 /**
  * Filter that can be used with the {@link org.apache.camel.spi.PackageScanClassResolver} resolver.
  */
+@FunctionalInterface
 public interface PackageScanFilter {
 
     /**
diff --git a/core/camel-base/src/main/java/org/apache/camel/impl/scan/AssignableToPackageScanFilter.java b/core/camel-base/src/main/java/org/apache/camel/impl/scan/AssignableToPackageScanFilter.java
index 892b8fd..56be02b 100644
--- a/core/camel-base/src/main/java/org/apache/camel/impl/scan/AssignableToPackageScanFilter.java
+++ b/core/camel-base/src/main/java/org/apache/camel/impl/scan/AssignableToPackageScanFilter.java
@@ -26,6 +26,7 @@ import org.apache.camel.spi.PackageScanFilter;
  */
 public class AssignableToPackageScanFilter implements PackageScanFilter {
     private final Set<Class<?>> parents = new HashSet<>();
+    private boolean includeInterfaces;
 
     public AssignableToPackageScanFilter() {
     }
@@ -38,13 +39,24 @@ public class AssignableToPackageScanFilter implements PackageScanFilter {
         this.parents.addAll(parents);
     }
 
+    public boolean isIncludeInterfaces() {
+        return includeInterfaces;
+    }
+
+    public void setIncludeInterfaces(boolean includeInterfaces) {
+        this.includeInterfaces = includeInterfaces;
+    }
+
     public void addParentType(Class<?> parentType) {
         parents.add(parentType);
     }
 
     public boolean matches(Class<?> type) {
-        if (parents != null && parents.size() > 0) {
+        if (parents.size() > 0) {
             for (Class<?> parent : parents) {
+                if (!includeInterfaces && parent.isInterface()) {
+                    continue;
+                }
                 if (parent.isAssignableFrom(type)) {
                     return true;
                 }
diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java b/core/camel-core/src/test/java/org/apache/camel/support/MyBarImplementation.java
similarity index 59%
copy from core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java
copy to core/camel-core/src/test/java/org/apache/camel/support/MyBarImplementation.java
index d87f1be..2a91dec 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java
+++ b/core/camel-core/src/test/java/org/apache/camel/support/MyBarImplementation.java
@@ -1,31 +1,43 @@
-/*
+/**
  * 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
- *
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
  * 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.spi;
+package org.apache.camel.support;
 
-/**
- * Filter that can be used with the {@link org.apache.camel.spi.PackageScanClassResolver} resolver.
- */
-public interface PackageScanFilter {
+public class MyBarImplementation implements MyBarInterface {
+
+    private String name;
+    private String city;
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String getCity() {
+        return city;
+    }
 
-    /**
-     * Does the given class match
-     *
-     * @param type the class
-     * @return true to include this class, false to skip it.
-     */
-    boolean matches(Class<?> type);
+    @Override
+    public void setCity(String city) {
+        this.city = city;
+    }
 }
diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java b/core/camel-core/src/test/java/org/apache/camel/support/MyBarInterface.java
similarity index 64%
copy from core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java
copy to core/camel-core/src/test/java/org/apache/camel/support/MyBarInterface.java
index d87f1be..5813eaa 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/PackageScanFilter.java
+++ b/core/camel-core/src/test/java/org/apache/camel/support/MyBarInterface.java
@@ -1,31 +1,29 @@
-/*
+/**
  * 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
- *
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
  * 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.spi;
+package org.apache.camel.support;
 
-/**
- * Filter that can be used with the {@link org.apache.camel.spi.PackageScanClassResolver} resolver.
- */
-public interface PackageScanFilter {
+public interface MyBarInterface {
+
+    void setName(String name);
+
+    String getName();
+
+    void setCity(String city);
 
-    /**
-     * Does the given class match
-     *
-     * @param type the class
-     * @return true to include this class, false to skip it.
-     */
-    boolean matches(Class<?> type);
+    String getCity();
+    
 }
diff --git a/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportAutowireClasspathTest.java b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportAutowireClasspathTest.java
new file mode 100644
index 0000000..d803585
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/support/PropertyBindingSupportAutowireClasspathTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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;
+
+import org.apache.camel.ContextTestSupport;
+import org.junit.Test;
+
+/**
+ * Unit test for PropertyBindingSupport
+ */
+public class PropertyBindingSupportAutowireClasspathTest extends ContextTestSupport {
+
+    @Test
+    public void testAutowireProperties() throws Exception {
+        Foo foo = new Foo();
+
+        PropertyBindingSupport.bindProperty(context, foo, "name", "James");
+        PropertyBindingSupport.autowireInterfacePropertiesFromClasspath(context, foo);
+        PropertyBindingSupport.bindProperty(context, foo, "my-bar.name", "Thirsty Bear");
+        PropertyBindingSupport.bindProperty(context, foo, "my-bar.city", "San Francisco");
+
+        assertEquals("James", foo.getName());
+        assertEquals("Thirsty Bear", foo.getMyBar().getName());
+        assertEquals("San Francisco", foo.getMyBar().getCity());
+    }
+
+    public static class Foo {
+        private String name;
+        private MyBarInterface myBar;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public MyBarInterface getMyBar() {
+            return myBar;
+        }
+
+        public void setMyBar(MyBarInterface myBar) {
+            this.myBar = myBar;
+        }
+    }
+
+
+}
+
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 081312f..9f84c43 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,16 +16,27 @@
  */
 package org.apache.camel.support;
 
+import java.io.File;
+import java.io.IOException;
 import java.lang.reflect.Method;
+import java.net.JarURLConnection;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
 
 import org.apache.camel.CamelContext;
+import org.apache.camel.ExtendedCamelContext;
 import org.apache.camel.PropertyBindingException;
+import org.apache.camel.spi.PackageScanClassResolver;
 
 import static org.apache.camel.support.IntrospectionSupport.findSetterMethods;
 import static org.apache.camel.util.ObjectHelper.isNotEmpty;
@@ -148,6 +159,7 @@ public final class PropertyBindingSupport {
         void onAutowire(Object target, String propertyName, Class propertyType, Object value);
 
     }
+
     /**
      * This will discover all the properties on the target, and automatic bind the properties that are null by
      * looking up in the registry to see if there is a single instance of the same type as the property.
@@ -193,8 +205,7 @@ public final class PropertyBindingSupport {
 
     private static boolean doAutowireSingletonPropertiesFromRegistry(CamelContext camelContext, Object target, Set<Object> parents,
                                                                      boolean bindNullOnly, boolean deepNesting, OnAutowiring callback) throws Exception {
-        // when adding a component then support auto-configuring complex types
-        // by looking up from registry, such as DataSource etc
+
         Map<String, Object> properties = new LinkedHashMap<>();
         IntrospectionSupport.getProperties(target, properties, null);
 
@@ -259,6 +270,130 @@ public final class PropertyBindingSupport {
     }
 
     /**
+     * This will discover all the properties on the target which are interfaces, and automatic attempt to bind the properties that are null by
+     * doing classpath scanning to find if there is a just only one class that implements the interface, and then attempt to create a new instance
+     * of this class. 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
+     */
+    public static boolean autowireInterfacePropertiesFromClasspath(CamelContext camelContext, Object target) {
+        return autowireInterfacePropertiesFromClasspath(camelContext, target, false, false, null);
+    }
+
+    /**
+     * This will discover all the properties on the target which are interfaces, and automatic attempt to bind the properties that are null by
+     * doing classpath scanning to find if there is a just only one class that implements the interface, and then attempt to create a new instance
+     * of this class. 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
+     */
+    public static boolean autowireInterfacePropertiesFromClasspath(CamelContext camelContext, Object target,
+                                                                   boolean bindNullOnly, boolean deepNesting, OnAutowiring callback) {
+        try {
+            if (target != null) {
+                Set<Object> parents = new HashSet<>();
+                return doAutowireInterfacePropertiesFromClasspath(camelContext, target, parents, bindNullOnly, deepNesting, callback);
+            }
+        } catch (Exception e) {
+            throw new PropertyBindingException(target, e);
+        }
+
+        return false;
+    }
+
+    private static boolean doAutowireInterfacePropertiesFromClasspath(CamelContext camelContext, Object target, Set<Object> parents,
+                                                                      boolean bindNullOnly, boolean deepNesting, OnAutowiring callback) throws Exception {
+
+        Map<String, Object> properties = new LinkedHashMap<>();
+        IntrospectionSupport.getProperties(target, properties, null);
+
+        boolean hit = false;
+
+        for (Map.Entry<String, Object> entry : properties.entrySet()) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            Class<?> type = getGetterType(target, key);
+
+            boolean skip = parents.contains(value) || value instanceof CamelContext;
+            if (skip) {
+                // we have already covered this as parent of parents so dont walk down this as we want to avoid
+                // circular dependencies when walking the OGNL graph, also we dont want to walk down CamelContext
+                continue;
+            }
+
+            if (isComplexUserType(type) && isInterface(type)) {
+                // if the property has not been set and its a complex type (not simple or string etc)
+                if (!bindNullOnly || value == null) {
+                    // do classpath scanning (TODO: do this only once)
+                    Set<String> packageNames = findAllPackageNames(null);
+                    if (!packageNames.isEmpty()) {
+                        String[] packages = packageNames.toArray(new String[packageNames.size()]);
+
+                        PackageScanClassResolver resolver = camelContext.adapt(ExtendedCamelContext.class).getPackageScanClassResolver();
+                        Set<Class<?>> classes = resolver.findByFilter(
+                                c -> !c.isInterface() && type.isAssignableFrom(c),
+                                packages);
+
+                        if (classes.size() == 1) {
+                            Class<?> clazz = classes.iterator().next();
+                            try {
+                                value = camelContext.getInjector().newInstance(clazz);
+                            } catch (Throwable e) {
+                                // ignore
+                            }
+                            if (value != null) {
+                                hit |= IntrospectionSupport.setProperty(camelContext, target, key, value);
+                                if (hit && callback != null) {
+                                    callback.onAutowire(target, key, type, value);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // attempt to create new instances to walk down the tree if its null (deepNesting option)
+                if (value == null && deepNesting) {
+                    // okay is there a setter so we can create a new instance and set it automatic
+                    Method method = findBestSetterMethod(target.getClass(), key, true);
+                    if (method != null) {
+                        Class<?> parameterType = method.getParameterTypes()[0];
+                        if (parameterType != null && org.apache.camel.util.ObjectHelper.hasDefaultPublicNoArgConstructor(parameterType)) {
+                            Object instance = camelContext.getInjector().newInstance(parameterType);
+                            if (instance != null) {
+                                org.apache.camel.support.ObjectHelper.invokeMethod(method, target, instance);
+                                target = instance;
+                                // remember this as parent and also autowire nested properties
+                                // do not walk down if it point to our-selves (circular reference)
+                                parents.add(target);
+                                value = instance;
+                                hit |= doAutowireInterfacePropertiesFromClasspath(camelContext, value, parents, bindNullOnly, deepNesting, callback);
+                            }
+                        }
+                    }
+                } else if (value != null) {
+                    // remember this as parent and also autowire nested properties
+                    // do not walk down if it point to our-selves (circular reference)
+                    parents.add(target);
+                    hit |= doAutowireInterfacePropertiesFromClasspath(camelContext, value, parents, bindNullOnly, deepNesting, callback);
+                }
+            }
+        }
+
+        return hit;
+    }
+
+    /**
      * Binds the properties to the target object, and removes the property that was bound from properties.
      *
      * @param camelContext  the camel context
@@ -572,6 +707,11 @@ public final class PropertyBindingSupport {
         return type != null && !type.isPrimitive() && !type.getName().startsWith("java");
     }
 
+    private static boolean isInterface(Class type) {
+        // lets consider all non java, as complex types
+        return type != null && type.isInterface();
+    }
+
     private static void setReferenceProperties(CamelContext context, Object target, Map<String, Object> parameters) {
         Iterator<Map.Entry<String, Object>> it = parameters.entrySet().iterator();
         while (it.hasNext()) {
@@ -609,4 +749,79 @@ public final class PropertyBindingSupport {
         return parameter != null && parameter.trim().startsWith("#");
     }
 
+    // TODO: move this to some util class
+
+    public static void main(String[] args) throws IOException {
+        PropertyBindingSupport.findAllPackageNames(null);
+    }
+
+    public static Set<String> findAllPackageNames(ClassLoader loader) throws IOException {
+        Set<String> answer = new TreeSet<>();
+
+        // get all JARs on classpath
+        String cp = System.getProperty("java.class.path");
+//        System.out.println(cp);
+        String[] parts = cp.split(":");
+        for (String p : parts) {
+            System.out.println(p);
+            if (p.endsWith(".jar")) {
+                JarFile jar = new JarFile(p);
+                jar.stream().forEach(e -> {
+                    if (e.isDirectory()) {
+                        String name = e.getName();
+                        name = name.replace('/', '.');
+                        name = name.replace('\\', '.');
+                        if (name.endsWith(".")) {
+                            name = name.substring(0, name.length() - 1);
+                        }
+                        if (validName(name)) {
+                            answer.add(name);
+                        }
+                    }
+                });
+            } else {
+                // its a directory such as target, then traverse and find all directory
+                gatherAllDirectories(new File(p), "", answer);
+            }
+        }
+
+        System.out.println("There are " + answer.size() + " packages");
+        answer.forEach(System.out::println);
+        return answer;
+    }
+
+    public static boolean validName(String name) {
+        boolean invalid = name.startsWith("META-INF") || name.startsWith("java") || name.startsWith("jdk") || name.startsWith("netscape")
+                || name.startsWith("resources") || name.startsWith("toolbarButtonGraphics")
+                || name.startsWith("oracle") || name.startsWith("sun") || name.startsWith("com.sun") || name.startsWith("com.oracle");
+        return !invalid;
+    }
+
+    public static boolean validPackageForClassloader(String packageName, ClassLoader loader) throws IOException {
+        return loader.getResources(packageName) != null;
+    }
+
+    public static void gatherAllDirectories(File path, String root, Set<String> dirs) {
+        if (path == null) {
+            return;
+        }
+        File[] paths = path.listFiles(f -> f.isDirectory());
+        if (paths != null) {
+            for (File dir : paths) {
+                String name = root + (root.isEmpty() ? "" : ".") + dir.getName();
+                name = name.replace('/', '.');
+                name = name.replace('\\', '.');
+                if (name.endsWith(".")) {
+                    name = name.substring(0, name.length() - 1);
+                }
+
+                if (validName(name)) {
+                    dirs.add(name);
+                    String subRoot = root + (root.isEmpty() ? "" : ".") + dir.getName();
+                    gatherAllDirectories(dir, subRoot, dirs);
+                }
+            }
+        }
+    }
+
 }