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 <claus.ib...@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); + } + } + } + } + }