From e58e71a0980e8a87d51903006c7a7cba6f317135 Mon Sep 17 00:00:00 2001 From: Decebal Suiu Date: Wed, 11 Jun 2014 13:30:30 +0300 Subject: [PATCH] load extensions from classpath; before this commit only plugins can declare extensions --- README.md | 2 + .../main/java/ro/fortsoft/pf4j/demo/Boot.java | 9 ++- .../fortsoft/pf4j/demo/WhazzupGreeting.java | 29 ++++++++ demo/app/src/main/resources/log4j.properties | 1 + .../fortsoft/pf4j/DefaultExtensionFinder.java | 69 +++++++++++++++---- .../ro/fortsoft/pf4j/PluginClassLoader.java | 59 +++++++++++----- 6 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 demo/app/src/main/java/ro/fortsoft/pf4j/demo/WhazzupGreeting.java diff --git a/README.md b/README.md index 9ac353b..cfa8a13 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,8 @@ The output is: >>> Welcome >>> Hello +**NOTE:** Starting with version 0.9 you can define an extension directly in a jar (in classpath). See [WhazzupGreeting](https://github.com/decebals/pf4j/blob/master/demo/app/src/main/java/ro/fortsoft/pf4j/WhazzupGreeting.java) for a real example. + You can inject your custom component (for example PluginDescriptorFinder, ExtensionFinder, PluginClasspath, ...) in DefaultPluginManager just override `create...` methods (factory method pattern). Example: diff --git a/demo/app/src/main/java/ro/fortsoft/pf4j/demo/Boot.java b/demo/app/src/main/java/ro/fortsoft/pf4j/demo/Boot.java index 453e2c2..2619531 100644 --- a/demo/app/src/main/java/ro/fortsoft/pf4j/demo/Boot.java +++ b/demo/app/src/main/java/ro/fortsoft/pf4j/demo/Boot.java @@ -52,12 +52,19 @@ public class Boot { System.out.println(">>> " + greeting.getGreeting()); } + // print extensions from classpath (non plugin) + System.out.println(String.format("Extensions added by classpath:")); + Set extensionClassNames = pluginManager.getExtensionClassNames(null); + for (String extension : extensionClassNames) { + System.out.println(" " + extension); + } + // print extensions for each started plugin List startedPlugins = pluginManager.getStartedPlugins(); for (PluginWrapper plugin : startedPlugins) { String pluginId = plugin.getDescriptor().getPluginId(); System.out.println(String.format("Extensions added by plugin '%s':", pluginId)); - Set extensionClassNames = pluginManager.getExtensionClassNames(pluginId); + extensionClassNames = pluginManager.getExtensionClassNames(pluginId); for (String extension : extensionClassNames) { System.out.println(" " + extension); } diff --git a/demo/app/src/main/java/ro/fortsoft/pf4j/demo/WhazzupGreeting.java b/demo/app/src/main/java/ro/fortsoft/pf4j/demo/WhazzupGreeting.java new file mode 100644 index 0000000..9cf7e35 --- /dev/null +++ b/demo/app/src/main/java/ro/fortsoft/pf4j/demo/WhazzupGreeting.java @@ -0,0 +1,29 @@ +/* + * Copyright 2014 Decebal Suiu + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with + * the License. You may obtain a copy of the License in the LICENSE file, or 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 ro.fortsoft.pf4j.demo; + +import ro.fortsoft.pf4j.Extension; +import ro.fortsoft.pf4j.demo.api.Greeting; + +/** + * @author Decebal Suiu + */ +@Extension +public class WhazzupGreeting implements Greeting { + + @Override + public String getGreeting() { + return "Whazzup"; + } + +} diff --git a/demo/app/src/main/resources/log4j.properties b/demo/app/src/main/resources/log4j.properties index 066d5e5..16ce034 100644 --- a/demo/app/src/main/resources/log4j.properties +++ b/demo/app/src/main/resources/log4j.properties @@ -4,6 +4,7 @@ log4j.rootLogger=DEBUG, Console # PF4J log # log4j.logger.ro.fortsoft.pf4j=DEBUG, Console +log4j.logger.ro.fortsoft.pf4j.PluginClassLoader=WARN, Console log4j.additivity.ro.fortsoft.pf4j=false # diff --git a/pf4j/src/main/java/ro/fortsoft/pf4j/DefaultExtensionFinder.java b/pf4j/src/main/java/ro/fortsoft/pf4j/DefaultExtensionFinder.java index 3234163..0f04ed7 100644 --- a/pf4j/src/main/java/ro/fortsoft/pf4j/DefaultExtensionFinder.java +++ b/pf4j/src/main/java/ro/fortsoft/pf4j/DefaultExtensionFinder.java @@ -57,16 +57,24 @@ public class DefaultExtensionFinder implements ExtensionFinder, PluginStateListe for (Map.Entry> entry : entries.entrySet()) { String pluginId = entry.getKey(); - PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginId); - if (PluginState.STARTED != pluginWrapper.getPluginState()) { - continue; + if (pluginId != null) { + PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginId); + if (PluginState.STARTED != pluginWrapper.getPluginState()) { + continue; + } } Set extensionClassNames = entry.getValue(); for (String className : extensionClassNames) { try { - Class extensionType = pluginManager.getPluginClassLoader(pluginId).loadClass(className); + Class extensionType; + if (pluginId != null) { + extensionType = pluginManager.getPluginClassLoader(pluginId).loadClass(className); + } else { + extensionType = getClass().getClassLoader().loadClass(className); + } + log.debug("Checking extension type '{}'", extensionType.getName()); if (type.isAssignableFrom(extensionType)) { Object instance = extensionFactory.create(extensionType); @@ -140,36 +148,71 @@ public class DefaultExtensionFinder implements ExtensionFinder, PluginStateListe return entries; } - entries = new HashMap>(); + entries = new LinkedHashMap>(); + + readClasspathIndexFiles(); + readPluginsIndexFiles(); + + return entries; + } + + private void readClasspathIndexFiles() { + log.debug("Reading extensions index files from classpath"); + + Set bucket = new HashSet(); + try { + Enumeration urls = getClass().getClassLoader().getResources(ExtensionsIndexer.EXTENSIONS_RESOURCE); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + log.debug("Read '{}'", url.getFile()); + Reader reader = new InputStreamReader(url.openStream(), "UTF-8"); + ExtensionsIndexer.readIndex(reader, bucket); + } + + if (bucket.isEmpty()) { + log.debug("No extensions found"); + } else { + log.debug("Found possible {} extensions:", bucket.size()); + for (String entry : bucket) { + log.debug(" " + entry); + } + } + + entries.put(null, bucket); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + private void readPluginsIndexFiles() { + log.debug("Reading extensions index files from plugins"); List plugins = pluginManager.getPlugins(); for (PluginWrapper plugin : plugins) { String pluginId = plugin.getDescriptor().getPluginId(); log.debug("Reading extensions index file for plugin '{}'", pluginId); - Set entriesPerPlugin = new HashSet(); + Set bucket = new HashSet(); try { URL url = plugin.getPluginClassLoader().getResource(ExtensionsIndexer.EXTENSIONS_RESOURCE); log.debug("Read '{}'", url.getFile()); Reader reader = new InputStreamReader(url.openStream(), "UTF-8"); - ExtensionsIndexer.readIndex(reader, entriesPerPlugin); + ExtensionsIndexer.readIndex(reader, bucket); - if (entriesPerPlugin.isEmpty()) { + if (bucket.isEmpty()) { log.debug("No extensions found"); } else { - log.debug("Found possible {} extensions:", entriesPerPlugin.size()); - for (String entry : entriesPerPlugin) { + log.debug("Found possible {} extensions:", bucket.size()); + for (String entry : bucket) { log.debug(" " + entry); } } - entries.put(pluginId, entriesPerPlugin); + entries.put(pluginId, bucket); } catch (IOException e) { log.error(e.getMessage(), e); } } - - return entries; } private boolean isExtensionPoint(Class type) { diff --git a/pf4j/src/main/java/ro/fortsoft/pf4j/PluginClassLoader.java b/pf4j/src/main/java/ro/fortsoft/pf4j/PluginClassLoader.java index 055c163..8167565 100644 --- a/pf4j/src/main/java/ro/fortsoft/pf4j/PluginClassLoader.java +++ b/pf4j/src/main/java/ro/fortsoft/pf4j/PluginClassLoader.java @@ -29,8 +29,6 @@ public class PluginClassLoader extends URLClassLoader { private static final Logger log = LoggerFactory.getLogger(PluginClassLoader.class); -// private static final String JAVA_PACKAGE_PREFIX = "java."; -// private static final String JAVAX_PACKAGE_PREFIX = "javax."; private static final String PLUGIN_PACKAGE_PREFIX = "ro.fortsoft.pf4j."; private PluginManager pluginManager; @@ -48,41 +46,45 @@ public class PluginClassLoader extends URLClassLoader { super.addURL(url); } + /** + * This implementation of loadClass uses a child first delegation model rather than the standard parent first. + * If the requested class cannot be found in this class loader, the parent class loader will be consulted + * via the standard ClassLoader.loadClass(String) mechanism. + */ @Override public Class loadClass(String className) throws ClassNotFoundException { -// System.out.println(">>>" + className); - - /* - // javax.mail is not in JDK ?! - // first check whether it's a system class, delegate to the system loader - if (className.startsWith(JAVA_PACKAGE_PREFIX) || className.startsWith(JAVAX_PACKAGE_PREFIX)) { - return findSystemClass(className); - } - */ - + log.debug("Received request to load class '{}'", className); // if the class it's a part of the plugin engine use parent class loader if (className.startsWith(PLUGIN_PACKAGE_PREFIX)) { + log.debug("Delegate the loading of class '{}' to parent", className); try { - return PluginClassLoader.class.getClassLoader().loadClass(className); + return getClass().getClassLoader().loadClass(className); } catch (ClassNotFoundException e) { // try next step + // TODO if I uncomment below lines (the correct approach) I received ClassNotFoundException for demo (ro.fortsoft.pf4j.demo) +// log.error(e.getMessage(), e); +// throw e; } } // second check whether it's already been loaded - Class loadedClass = findLoadedClass(className); - if (loadedClass != null) { - return loadedClass; + Class clazz = findLoadedClass(className); + if (clazz != null) { + log.debug("Found loaded class '{}'", className); + return clazz; } // nope, try to load locally try { - return findClass(className); + clazz = findClass(className); + log.debug("Found class '{}' in plugin classpath", className); + return clazz; } catch (ClassNotFoundException e) { // try next step } // look in dependencies + log.debug("Look in dependencies for class '{}'", className); List dependencies = pluginDescriptor.getDependencies(); for (PluginDependency dependency : dependencies) { PluginClassLoader classLoader = pluginManager.getPluginClassLoader(dependency.getPluginId()); @@ -93,10 +95,33 @@ public class PluginClassLoader extends URLClassLoader { } } + log.debug("Couldn't find class '{}' in plugin classpath. Delegating to parent"); + // use the standard URLClassLoader (which follows normal parent delegation) return super.loadClass(className); } + /** + * Load the named resource from this plugin. This implementation checks the plugin's classpath first + * then delegates to the parent. + * + * @param name the name of the resource. + * @return the URL to the resource, null if the resource was not found. + */ + @Override + public URL getResource(String name) { + log.debug("Trying to find resource '{}' in plugin classpath", name); + URL url = findResource(name); + if (url != null) { + log.debug("Found resource '{}' in plugin classpath", name); + return url; + } + + log.debug("Couldn't find resource '{}' in plugin classpath. Delegating to parent"); + + return super.getResource(name); + } + /** * Release all resources acquired by this class loader. * The current implementation is incomplete.