From 2aeb77b8c8e6023651d1fc7b9ef31736b855cafa Mon Sep 17 00:00:00 2001 From: Decebal Suiu Date: Thu, 11 Oct 2012 13:29:43 +0300 Subject: [PATCH] first commit --- README.md | 151 ++++++++ demo/api/pom.xml | 44 +++ .../main/java/org/pf4j/demo/api/Greeting.java | 24 ++ demo/app/pom.xml | 89 +++++ demo/app/src/main/assembly/assembly.xml | 32 ++ .../app/src/main/java/org/pf4j/demo/Boot.java | 63 ++++ demo/app/src/main/resources/log4j.properties | 10 + .../app/src/main/resources/logging.properties | 8 + demo/plugin1/plugin.properties | 5 + demo/plugin1/pom.xml | 127 +++++++ demo/plugin1/src/main/assembly/assembly.xml | 37 ++ .../org/pf4j/demo/welcome/WelcomePlugin.java | 46 +++ demo/plugin2/plugin.properties | 5 + demo/plugin2/pom.xml | 136 ++++++++ demo/plugin2/src/main/assembly/assembly.xml | 37 ++ .../java/org/pf4j/demo/hello/HelloPlugin.java | 48 +++ demo/pom.xml | 60 ++++ pf4j/pom.xml | 122 +++++++ .../java/org/pf4j/DefaultExtensionFinder.java | 80 +++++ .../pf4j/DefaultPluginDescriptorFinder.java | 90 +++++ .../java/org/pf4j/DefaultPluginManager.java | 327 ++++++++++++++++++ .../java/org/pf4j/DependencyResolver.java | 81 +++++ pf4j/src/main/java/org/pf4j/Extension.java | 35 ++ .../main/java/org/pf4j/ExtensionFinder.java | 24 ++ .../main/java/org/pf4j/ExtensionPoint.java | 20 ++ .../main/java/org/pf4j/ExtensionWrapper.java | 41 +++ pf4j/src/main/java/org/pf4j/Plugin.java | 68 ++++ .../main/java/org/pf4j/PluginClassLoader.java | 91 +++++ .../main/java/org/pf4j/PluginDescriptor.java | 140 ++++++++ .../java/org/pf4j/PluginDescriptorFinder.java | 28 ++ .../main/java/org/pf4j/PluginException.java | 40 +++ pf4j/src/main/java/org/pf4j/PluginLoader.java | 142 ++++++++ .../src/main/java/org/pf4j/PluginManager.java | 49 +++ .../src/main/java/org/pf4j/PluginVersion.java | 191 ++++++++++ .../src/main/java/org/pf4j/PluginWrapper.java | 61 ++++ .../java/org/pf4j/util/DirectedGraph.java | 171 +++++++++ .../java/org/pf4j/util/DirectoryFilter.java | 35 ++ .../java/org/pf4j/util/ExtensionFilter.java | 41 +++ .../main/java/org/pf4j/util/JarFilter.java | 32 ++ .../java/org/pf4j/util/UberClassLoader.java | 72 ++++ pf4j/src/main/java/org/pf4j/util/Unzip.java | 122 +++++++ .../main/java/org/pf4j/util/ZipFilter.java | 32 ++ pom.xml | 72 ++++ run-demo.sh | 24 ++ 44 files changed, 3153 insertions(+) create mode 100644 README.md create mode 100644 demo/api/pom.xml create mode 100644 demo/api/src/main/java/org/pf4j/demo/api/Greeting.java create mode 100644 demo/app/pom.xml create mode 100644 demo/app/src/main/assembly/assembly.xml create mode 100644 demo/app/src/main/java/org/pf4j/demo/Boot.java create mode 100644 demo/app/src/main/resources/log4j.properties create mode 100644 demo/app/src/main/resources/logging.properties create mode 100644 demo/plugin1/plugin.properties create mode 100644 demo/plugin1/pom.xml create mode 100644 demo/plugin1/src/main/assembly/assembly.xml create mode 100644 demo/plugin1/src/main/java/org/pf4j/demo/welcome/WelcomePlugin.java create mode 100644 demo/plugin2/plugin.properties create mode 100644 demo/plugin2/pom.xml create mode 100644 demo/plugin2/src/main/assembly/assembly.xml create mode 100644 demo/plugin2/src/main/java/org/pf4j/demo/hello/HelloPlugin.java create mode 100644 demo/pom.xml create mode 100644 pf4j/pom.xml create mode 100644 pf4j/src/main/java/org/pf4j/DefaultExtensionFinder.java create mode 100644 pf4j/src/main/java/org/pf4j/DefaultPluginDescriptorFinder.java create mode 100644 pf4j/src/main/java/org/pf4j/DefaultPluginManager.java create mode 100644 pf4j/src/main/java/org/pf4j/DependencyResolver.java create mode 100644 pf4j/src/main/java/org/pf4j/Extension.java create mode 100644 pf4j/src/main/java/org/pf4j/ExtensionFinder.java create mode 100644 pf4j/src/main/java/org/pf4j/ExtensionPoint.java create mode 100644 pf4j/src/main/java/org/pf4j/ExtensionWrapper.java create mode 100644 pf4j/src/main/java/org/pf4j/Plugin.java create mode 100644 pf4j/src/main/java/org/pf4j/PluginClassLoader.java create mode 100644 pf4j/src/main/java/org/pf4j/PluginDescriptor.java create mode 100644 pf4j/src/main/java/org/pf4j/PluginDescriptorFinder.java create mode 100644 pf4j/src/main/java/org/pf4j/PluginException.java create mode 100644 pf4j/src/main/java/org/pf4j/PluginLoader.java create mode 100644 pf4j/src/main/java/org/pf4j/PluginManager.java create mode 100644 pf4j/src/main/java/org/pf4j/PluginVersion.java create mode 100644 pf4j/src/main/java/org/pf4j/PluginWrapper.java create mode 100644 pf4j/src/main/java/org/pf4j/util/DirectedGraph.java create mode 100644 pf4j/src/main/java/org/pf4j/util/DirectoryFilter.java create mode 100644 pf4j/src/main/java/org/pf4j/util/ExtensionFilter.java create mode 100644 pf4j/src/main/java/org/pf4j/util/JarFilter.java create mode 100644 pf4j/src/main/java/org/pf4j/util/UberClassLoader.java create mode 100644 pf4j/src/main/java/org/pf4j/util/Unzip.java create mode 100644 pf4j/src/main/java/org/pf4j/util/ZipFilter.java create mode 100644 pom.xml create mode 100755 run-demo.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa038a2 --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +Plugin Framework for Java (PF4J) +===================== + +A plugin is a way for a third party to extend the functionality of an application. A plugin implements extensions points +declared by application or another plugins. Also a plugin can defines extension points. + +Components +------------------- +- **Plugin** is the base class for all plugins types. Each plugin is loaded into a separate class loader to avoid conflicts. +- **PluginManager** is used for all aspects of plugins management (loading, starting, stopping). +- **ExtensionPoint** is a point in the application where custom code can be invoked. +- **Extension** is an implementation of extension point. + +Artifacts +------------------- +- PF4J `pf4j` (jar) +- PF4J Demo `pf4j-demo` (executable jar) + +Using Maven +------------------- + +First you must install the pf4j artifacts in your local maven repository with: + + mvn clean install + +I will upload these artifacts in maven central repository as soon as possible. + +In your pom.xml you must define the dependencies to PF4J artifacts with: + +```xml + + org.pf4j + pf4j + ${pf4j.version} + +``` + +where ${pf4j.version} is the last pf4j version. + +How to use +------------------- +It's very simple to add pf4j in your application: + + public static void main(String[] args) { + ... + + PluginManager pluginManager = new DefaultPluginManager(); + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + + ... + } + +In above code, I created a DefaultPluginManager (it's the default implementation for +PluginManager interface) that load and start all active(resolved) plugins. +The plugins are stored in a folder. You can specify the plugins folder in constructor of DefaultPluginManager +or using the constructor without parameters (in this case plugins folder is returned by System.getProperty("pf4j.pluginsDir", "plugins")). + +The structure of plugins folder is: +- plugin1.zip (or plugin1 folder) +- plugin2.zip (or plugin2 folder) +... +- pluginN.zip (or pluginN folder) + +In plugins folder you can put a plugin as folder or archive file (zip). +A plugin folder has this structure: +- `classes` folder +- `lib` folder (optional - if the plugin used third party libraries) + +The plugin manager discovers plugins metadata using a PluginDescriptorFinder. DefaultPluginDescriptorFinder lookup plugin descriptor in MANIFEST.MF file. +In this case the classes/META-INF/MANIFEST.MF looks like: + + Manifest-Version: 1.0 + Archiver-Version: Plexus Archiver + Created-By: Apache Maven + Built-By: decebal + Build-Jdk: 1.6.0_17 + Plugin-Class: org.pf4j.demo.welcome.WelcomePlugin + Plugin-Dependencies: x, y z + Plugin-Id: welcome-plugin + Plugin-Provider: Decebal Suiu + Plugin-Version: 0.0.1 + +In above manifest I described a plugin with id `welcome-plugin`, with class `org.pf4j.demo.welcome.WelcomePlugin`, with version `0.0.1` and with dependencies +to plugins `x, y, z`. + +You can define an extension point in your application using ExtensionPoint interface marker. + + public interface Greeting extends ExtensionPoint { + + public String getGreeting(); + + } + +Another important internal component is ExtensionFinder that describes how plugin manager discovers extensions for extensions points. DefaultExtensionFinder look up extensions using Extension annotation. + + public class WelcomePlugin extends Plugin { + + public WelcomePlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Extension + public static class WelcomeGreeting implements Greeting { + + public String getGreeting() { + return "Welcome"; + } + + } + + } + +In above code I supply an extension for the Greeting extension point. + +You can retrieves all extensions for an extension point with: + + List> greetings = pluginManager.getExtensions(Greeting.class); + for (ExtensionWrapper greeting : greetings) { + System.out.println(">>> " + greeting.getInstance().getGreeting()); + } + + +For more information please see the demo sources. + +Demo +------------------- + +I have a tiny demo application. In this demo I have implemented two widgets types: +a chart widget (using open flash chart) and a text widget (display a Lorem Ipsum). +You can drag and drop widgets, perform some actions on each widget, add or remove new +widgets, change widget settings, collapse widgets. + +The demo application is in demo folder. +To run the demo application use: + + ./run-demo.sh + +License +-------------- + +Copyright 2012 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. diff --git a/demo/api/pom.xml b/demo/api/pom.xml new file mode 100644 index 0000000..1514476 --- /dev/null +++ b/demo/api/pom.xml @@ -0,0 +1,44 @@ + + + + + org.pf4j.demo + pom + 0.1-SNAPSHOT + + + 4.0.0 + pf4j-demo-api + 0.1-SNAPSHOT + jar + Demo Api + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.pf4j + pf4j + ${project.version} + provided + + + + diff --git a/demo/api/src/main/java/org/pf4j/demo/api/Greeting.java b/demo/api/src/main/java/org/pf4j/demo/api/Greeting.java new file mode 100644 index 0000000..b21f80c --- /dev/null +++ b/demo/api/src/main/java/org/pf4j/demo/api/Greeting.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012 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 org.pf4j.demo.api; + +import org.pf4j.ExtensionPoint; + +/** + * @author Decebal Suiu + */ +public interface Greeting extends ExtensionPoint { + + public String getGreeting(); + +} diff --git a/demo/app/pom.xml b/demo/app/pom.xml new file mode 100644 index 0000000..d9084b1 --- /dev/null +++ b/demo/app/pom.xml @@ -0,0 +1,89 @@ + + + + + org.pf4j.demo + pom + 0.1-SNAPSHOT + + + 4.0.0 + pf4j-demo-app + 0.1-SNAPSHOT + jar + Demo App + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + org.pf4j.demo.Boot + + + + + + maven-assembly-plugin + 2.3 + + + + src/main/assembly/assembly.xml + + + false + + + + make-assembly + package + + attached + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.3.1 + + + + true + lib/ + ${main.class} + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.pf4j + pf4j + ${project.version} + + + org.pf4j.demo + pf4j-demo-api + ${project.version} + + + + diff --git a/demo/app/src/main/assembly/assembly.xml b/demo/app/src/main/assembly/assembly.xml new file mode 100644 index 0000000..1d9e8e4 --- /dev/null +++ b/demo/app/src/main/assembly/assembly.xml @@ -0,0 +1,32 @@ + + + plugin + + dir + + false + + + false + lib + + *:jar:* + + + + + + ${project.build.directory} + + + *.jar + + + + + diff --git a/demo/app/src/main/java/org/pf4j/demo/Boot.java b/demo/app/src/main/java/org/pf4j/demo/Boot.java new file mode 100644 index 0000000..b34af53 --- /dev/null +++ b/demo/app/src/main/java/org/pf4j/demo/Boot.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012 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 org.pf4j.demo; + +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.pf4j.DefaultPluginManager; +import org.pf4j.ExtensionWrapper; +import org.pf4j.PluginManager; +import org.pf4j.demo.api.Greeting; + +/** + * A boot class that start the demo. + * + * @author Decebal Suiu + */ +public class Boot { + + public static void main(String[] args) { + // print logo + printLogo(); + + // load and start (active/resolved) plugins + final PluginManager pluginManager = new DefaultPluginManager(); + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + + List> greetings = pluginManager.getExtensions(Greeting.class); + for (ExtensionWrapper greeting : greetings) { + System.out.println(">>> " + greeting.getInstance().getGreeting()); + } + + pluginManager.stopPlugins(); + /* + Runtime.getRuntime().addShutdownHook(new Thread() { + + @Override + public void run() { + pluginManager.stopPlugins(); + } + + }); + */ + } + + private static void printLogo() { + System.out.println(StringUtils.repeat("#", 40)); + System.out.println(StringUtils.center("PF4J-DEMO", 40)); + System.out.println(StringUtils.repeat("#", 40)); + } + +} diff --git a/demo/app/src/main/resources/log4j.properties b/demo/app/src/main/resources/log4j.properties new file mode 100644 index 0000000..b3e5aa8 --- /dev/null +++ b/demo/app/src/main/resources/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootLogger=DEBUG,Console + +log4j.appender.Console=org.apache.log4j.ConsoleAppender +log4j.appender.Console.layout=org.apache.log4j.PatternLayout +log4j.appender.Console.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n + +log4j.logger.org.apache.wicket=INFO +log4j.logger.org.apache.wicket.protocol.http.HttpSessionStore=INFO +log4j.logger.org.apache.wicket.version=INFO +log4j.logger.org.apache.wicket.RequestCycle=INFO diff --git a/demo/app/src/main/resources/logging.properties b/demo/app/src/main/resources/logging.properties new file mode 100644 index 0000000..6fe7cef --- /dev/null +++ b/demo/app/src/main/resources/logging.properties @@ -0,0 +1,8 @@ +#handlers = java.util.logging.ConsoleHandler +handlers = org.slf4j.bridge.SLF4JBridgeHandler + +# Set the default logging level for the root logger +.level = ALL + +# Set the default formatter for new ConsoleHandler instances +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter diff --git a/demo/plugin1/plugin.properties b/demo/plugin1/plugin.properties new file mode 100644 index 0000000..9da9bcc --- /dev/null +++ b/demo/plugin1/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=welcome-plugin +plugin.class=org.pf4j.demo.welcome.WelcomePlugin +plugin.version=0.0.1 +plugin.provider=Decebal Suiu +plugin.dependencies= diff --git a/demo/plugin1/pom.xml b/demo/plugin1/pom.xml new file mode 100644 index 0000000..10c1163 --- /dev/null +++ b/demo/plugin1/pom.xml @@ -0,0 +1,127 @@ + + + + + org.pf4j.demo + pom + 0.1-SNAPSHOT + + + 4.0.0 + pf4j-demo-plugin1 + 0.1-SNAPSHOT + jar + Demo Plugin #1 + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0-alpha-2 + + + initialize + + read-project-properties + + + + plugin.properties + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.6 + + + unzip jar file + package + + + + + + + run + + + + + + + maven-assembly-plugin + 2.3 + + + + src/main/assembly/assembly.xml + + + false + + + + make-assembly + package + + attached + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.dependencies} + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.pf4j + pf4j + ${project.version} + provided + + + org.pf4j.demo + pf4j-demo-api + ${project.version} + provided + + + + diff --git a/demo/plugin1/src/main/assembly/assembly.xml b/demo/plugin1/src/main/assembly/assembly.xml new file mode 100644 index 0000000..3fdc464 --- /dev/null +++ b/demo/plugin1/src/main/assembly/assembly.xml @@ -0,0 +1,37 @@ + + + plugin + + zip + + false + + + false + runtime + lib + + *:jar:* + + + + + + + target/plugin-classes + classes + + + diff --git a/demo/plugin1/src/main/java/org/pf4j/demo/welcome/WelcomePlugin.java b/demo/plugin1/src/main/java/org/pf4j/demo/welcome/WelcomePlugin.java new file mode 100644 index 0000000..22ebf7d --- /dev/null +++ b/demo/plugin1/src/main/java/org/pf4j/demo/welcome/WelcomePlugin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012 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 org.pf4j.demo.welcome; + +import org.pf4j.Extension; +import org.pf4j.Plugin; +import org.pf4j.PluginWrapper; +import org.pf4j.demo.api.Greeting; + +/** + * @author Decebal Suiu + */ +public class WelcomePlugin extends Plugin { + + public WelcomePlugin(PluginWrapper wrapper) { + super(wrapper); + } + + public void start() { + System.out.println("WelcomePlugin.start()"); + } + + public void stop() { + System.out.println("WelcomePlugin.stop()"); + } + + @Extension + public static class WelcomeGreeting implements Greeting { + + public String getGreeting() { + return "Welcome"; + } + + } + +} diff --git a/demo/plugin2/plugin.properties b/demo/plugin2/plugin.properties new file mode 100644 index 0000000..60b6f33 --- /dev/null +++ b/demo/plugin2/plugin.properties @@ -0,0 +1,5 @@ +plugin.id=hello-plugin +plugin.class=org.pf4j.demo.hello.HelloPlugin +plugin.version=0.0.1 +plugin.provider=Decebal Suiu +plugin.dependencies= diff --git a/demo/plugin2/pom.xml b/demo/plugin2/pom.xml new file mode 100644 index 0000000..7f4f617 --- /dev/null +++ b/demo/plugin2/pom.xml @@ -0,0 +1,136 @@ + + + + + org.pf4j.demo + pom + 0.1-SNAPSHOT + + + 4.0.0 + pf4j-demo-plugin2 + 0.1-SNAPSHOT + jar + Demo Plugin #2 + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + org.codehaus.mojo + properties-maven-plugin + 1.0-alpha-2 + + + initialize + + read-project-properties + + + + plugin.properties + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.6 + + + unzip jar file + package + + + + + + + run + + + + + + + maven-assembly-plugin + 2.3 + + false + + + src/main/assembly/assembly.xml + + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.dependencies} + + + + + + make-assembly + package + + single + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.dependencies} + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.pf4j + pf4j + ${project.version} + provided + + + org.pf4j.demo + pf4j-demo-api + ${project.version} + provided + + + + diff --git a/demo/plugin2/src/main/assembly/assembly.xml b/demo/plugin2/src/main/assembly/assembly.xml new file mode 100644 index 0000000..5cefe0d --- /dev/null +++ b/demo/plugin2/src/main/assembly/assembly.xml @@ -0,0 +1,37 @@ + + + plugin + + zip + + false + + + false + runtime + lib + + *:jar:* + + + + + + + target/plugin-classes + classes + + + diff --git a/demo/plugin2/src/main/java/org/pf4j/demo/hello/HelloPlugin.java b/demo/plugin2/src/main/java/org/pf4j/demo/hello/HelloPlugin.java new file mode 100644 index 0000000..ad7fd0c --- /dev/null +++ b/demo/plugin2/src/main/java/org/pf4j/demo/hello/HelloPlugin.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012 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 org.pf4j.demo.hello; + +import org.pf4j.Extension; +import org.pf4j.Plugin; +import org.pf4j.PluginWrapper; +import org.pf4j.demo.api.Greeting; + +/** + * A very simple plugin. + * + * @author Decebal Suiu + */ +public class HelloPlugin extends Plugin { + + public HelloPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + public void start() { + System.out.println("HelloPlugin.start()"); + } + + public void stop() { + System.out.println("HelloPlugin.stop()"); + } + + @Extension + public static class HelloGreeting implements Greeting { + + public String getGreeting() { + return "Hello"; + } + + } + +} diff --git a/demo/pom.xml b/demo/pom.xml new file mode 100644 index 0000000..dbb029a --- /dev/null +++ b/demo/pom.xml @@ -0,0 +1,60 @@ + + + + + org.pf4j + pom + 0.1-SNAPSHOT + + + 4.0.0 + org.pf4j.demo + pom + 0.1-SNAPSHOT + pom + PF4J Demo + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + true + + + + + + + app + api + plugin1 + plugin2 + + + diff --git a/pf4j/pom.xml b/pf4j/pom.xml new file mode 100644 index 0000000..920a870 --- /dev/null +++ b/pf4j/pom.xml @@ -0,0 +1,122 @@ + + + + + org.pf4j + pom + 0.1-SNAPSHOT + + + 4.0.0 + pf4j + 0.1-SNAPSHOT + jar + PF4J Library + Plugin Framework for Java + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + scm:git:https://github.com/decebals/pf4j.git + scm:git:https://github.com/decebals/pf4j.git + git@github.com/decebals/pf4j.git + + + + UTF-8 + 1.6.4 + + + + + + false + src/main/java + + **/*.java + + + + src/main/resources + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + true + + + + + org.apache.maven.plugins + maven-war-plugin + 2.1.1 + + root + + + + + org.apache.maven.plugins + maven-release-plugin + 2.3 + + deploy + true + release-@{project.version} + + + + + + + + commons-lang + commons-lang + 2.4 + + + + net.java.sezpoz + sezpoz + 1.9 + + + + + log4j + log4j + 1.2.16 + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + diff --git a/pf4j/src/main/java/org/pf4j/DefaultExtensionFinder.java b/pf4j/src/main/java/org/pf4j/DefaultExtensionFinder.java new file mode 100644 index 0000000..e7a6dc9 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/DefaultExtensionFinder.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.java.sezpoz.Index; +import net.java.sezpoz.IndexItem; + +/** + * Using Sezpoz(http://sezpoz.java.net/) for extensions discovery. + * + * @author Decebal Suiu + */ +public class DefaultExtensionFinder implements ExtensionFinder { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultExtensionFinder.class); + + private volatile List> indices; + private ClassLoader classLoader; + + public DefaultExtensionFinder(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public List> find(Class type) { + LOG.debug("Find extensions for " + type); + List> result = new ArrayList>(); + getIndices(); +// System.out.println("indices = "+ indices); + for (IndexItem item : indices) { + try { + AnnotatedElement element = item.element(); + Class extensionType = (Class) element; + LOG.debug("Checking extension type " + extensionType); + if (type.isAssignableFrom(extensionType)) { + Object instance = item.instance(); + if (instance != null) { + LOG.debug("Added extension " + extensionType); + result.add(new ExtensionWrapper(type.cast(instance), item.annotation().ordinal())); + } + } + } catch (InstantiationException e) { + LOG.error(e.getMessage(), e); + } + } + + return result; + } + + private List> getIndices() { + if (indices == null) { + indices = new ArrayList>(); + Iterator> it = Index.load(Extension.class, Object.class, classLoader).iterator(); + while (it.hasNext()) { + indices.add(it.next()); + } + } + + return indices; + } + +} diff --git a/pf4j/src/main/java/org/pf4j/DefaultPluginDescriptorFinder.java b/pf4j/src/main/java/org/pf4j/DefaultPluginDescriptorFinder.java new file mode 100644 index 0000000..0055e57 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/DefaultPluginDescriptorFinder.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.apache.commons.lang.StringUtils; + +/** + * Read the plugin descriptor from the manifest file. + * + * @author Decebal Suiu + */ +public class DefaultPluginDescriptorFinder implements PluginDescriptorFinder { + + @Override + public PluginDescriptor find(File pluginRepository) throws PluginException { + // TODO it's ok with classes/ ? + File manifestFile = new File(pluginRepository, "classes/META-INF/MANIFEST.MF"); + if (!manifestFile.exists()) { + // not found a 'plugin.xml' file for this plugin + throw new PluginException("Cannot find '" + manifestFile + "' file"); + } + + FileInputStream input = null; + try { + input = new FileInputStream(manifestFile); + } catch (FileNotFoundException e) { + // not happening + } + + Manifest manifest = null; + try { + manifest = new Manifest(input); + } catch (IOException e) { + throw new PluginException(e.getMessage(), e); + } finally { + try { + input.close(); + } catch (IOException e) { + throw new PluginException(e.getMessage(), e); + } + } + + PluginDescriptor pluginDescriptor = new PluginDescriptor(); + + // TODO validate !!! + Attributes attrs = manifest.getMainAttributes(); + String id = attrs.getValue("Plugin-Id"); + if (StringUtils.isEmpty(id)) { + throw new PluginException("Plugin-Id cannot be empty"); + } + pluginDescriptor.setPluginId(id); + + String clazz = attrs.getValue("Plugin-Class"); + if (StringUtils.isEmpty(clazz)) { + throw new PluginException("Plugin-Class cannot be empty"); + } + pluginDescriptor.setPluginClass(clazz); + + String version = attrs.getValue("Plugin-Version"); + if (StringUtils.isEmpty(version)) { + throw new PluginException("Plugin-Version cannot be empty"); + } + pluginDescriptor.setPluginVersion(PluginVersion.createVersion(version)); + + String provider = attrs.getValue("Plugin-Provider"); + pluginDescriptor.setProvider(provider); + String dependencies = attrs.getValue("Plugin-Dependencies"); + pluginDescriptor.setDependencies(dependencies); + + return pluginDescriptor; + } + +} diff --git a/pf4j/src/main/java/org/pf4j/DefaultPluginManager.java b/pf4j/src/main/java/org/pf4j/DefaultPluginManager.java new file mode 100644 index 0000000..9cb7ccd --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/DefaultPluginManager.java @@ -0,0 +1,327 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.pf4j.util.DirectoryFilter; +import org.pf4j.util.UberClassLoader; +import org.pf4j.util.Unzip; +import org.pf4j.util.ZipFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Default implementation of the PluginManager interface. + * + * @author Decebal Suiu + */ +public class DefaultPluginManager implements PluginManager { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginManager.class); + + /** + * The plugins repository. + */ + private File pluginsDirectory; + + private ExtensionFinder extensionFinder; + + private PluginDescriptorFinder pluginDescriptorFinder; + + /** + * A map of plugins this manager is responsible for (the key is the 'pluginId'). + */ + private Map plugins; + + /** + * A map of plugin class loaders (he key is the 'pluginId'). + */ + private Map pluginClassLoaders; + + /** + * A relation between 'pluginPath' and 'pluginId' + */ + private Map pathToIdMap; + + /** + * A list with unresolved plugins (unresolved dependency). + */ + private List unresolvedPlugins; + + /** + * A list with resolved plugins (resolved dependency). + */ + private List resolvedPlugins; + + /** + * A list with disabled plugins. + */ + private List disabledPlugins; + + private UberClassLoader uberClassLoader; + + /** + * Th plugins directory is supplied by System.getProperty("pf4j.pluginsDir", "plugins"). + */ + public DefaultPluginManager() { + this(new File(System.getProperty("pf4j.pluginsDir", "plugins"))); + } + + /** + * Constructs DefaultPluginManager which the given plugins directory. + * + * @param pluginsDirectory + * the directory to search for plugins + */ + public DefaultPluginManager(File pluginsDirectory) { + this.pluginsDirectory = pluginsDirectory; + plugins = new HashMap(); + pluginClassLoaders = new HashMap(); + pathToIdMap = new HashMap(); + unresolvedPlugins = new ArrayList(); + resolvedPlugins = new ArrayList(); + disabledPlugins = new ArrayList(); + pluginDescriptorFinder = new DefaultPluginDescriptorFinder(); + uberClassLoader = new UberClassLoader(); + extensionFinder = new DefaultExtensionFinder(uberClassLoader); + } + + /** + * Retrieves all active plugins. + */ + public List getPlugins() { + return new ArrayList(plugins.values()); + } + + public List getResolvedPlugins() { + return resolvedPlugins; + } + + public Plugin getPlugin(String pluginId) { + return plugins.get(pluginId); + } + + public List getUnresolvedPlugins() { + return unresolvedPlugins; + } + + public List getDisabledPlugins() { + return disabledPlugins; + } + + /** + * Start all active plugins. + */ + public void startPlugins() { + List resolvedPlugins = getResolvedPlugins(); + for (Plugin plugin : resolvedPlugins) { + try { + plugin.start(); + } catch (PluginException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + /** + * Stop all active plugins. + */ + public void stopPlugins() { + List resolvedPlugins = getResolvedPlugins(); + for (Plugin plugin : resolvedPlugins) { + try { + plugin.stop(); + } catch (PluginException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + /** + * Load plugins. + */ + public void loadPlugins() { + // check for plugins directory + if (!pluginsDirectory.exists() || !pluginsDirectory.isDirectory()) { + LOG.error("No '" + pluginsDirectory + "' directory"); + return; + } + + // expand all plugin archives + FilenameFilter zipFilter = new ZipFilter(); + String[] zipFiles = pluginsDirectory.list(zipFilter); + for (String zipFile : zipFiles) { + try { + expandPluginArchive(zipFile); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + e.printStackTrace(); + } + } + + // load any plugin from plugins directory + FilenameFilter directoryFilter = new DirectoryFilter(); + String[] directories = pluginsDirectory.list(directoryFilter); + for (String directory : directories) { + try { + loadPlugin(directory); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + e.printStackTrace(); + } + } + + // check for no plugins + if (directories.length == 0) { + LOG.info("No plugins"); + return; + } + + // resolve 'unresolvedPlugins' + resolvePlugins(); + } + + /** + * Get plugin class loader for this path. + */ + public PluginClassLoader getPluginClassLoader(String pluginId) { + return pluginClassLoaders.get(pluginId); + } + + public List> getExtensions(Class type) { + return extensionFinder.find(type); + } + + private void loadPlugin(String fileName) throws Exception { + // test for plugin directory + File pluginDirectory = new File(pluginsDirectory, fileName); + if (!pluginDirectory.isDirectory()) { + return; + } + + // try to load the plugin + String pluginPath = "/".concat(fileName); + + // test for disabled plugin + if (disabledPlugins.contains(pluginPath)) { + return; + } + + // it's a new plugin + if (plugins.get(pathToIdMap.get(pluginPath)) != null) { + return; + } + + // retrieves the plugin descriptor + LOG.debug("Find plugin descriptor '" + pluginPath + "'"); + PluginDescriptor pluginDescriptor = pluginDescriptorFinder.find(pluginDirectory); + LOG.debug("Descriptor " + pluginDescriptor); + String pluginClassName = pluginDescriptor.getPluginClass(); + LOG.debug("Class '" + pluginClassName + "'" + " for plugin '" + pluginPath + "'"); + + // load plugin + LOG.debug("Loading plugin '" + pluginPath + "'"); + PluginWrapper pluginWrapper = new PluginWrapper(pluginDescriptor); + PluginLoader pluginLoader = new PluginLoader(this, pluginWrapper, pluginDirectory); + pluginLoader.load(); + LOG.debug("Loaded plugin '" + pluginPath + "'"); + + // set some variables in plugin wrapper + pluginWrapper.setPluginPath(pluginPath); + pluginWrapper.setPluginClassLoader(pluginLoader.getPluginClassLoader()); + + // create the plugin instance + LOG.debug("Creating instance for plugin '" + pluginPath + "'"); + Plugin plugin = getPluginInstance(pluginWrapper, pluginLoader); + LOG.debug("Created instance '" + plugin + "' for plugin '" + pluginPath + "'"); + + String pluginId = pluginDescriptor.getPluginId(); + + // add plugin to the list with plugins + plugins.put(pluginId, plugin); + unresolvedPlugins.add(plugin); + + // add plugin class loader to the list with class loaders + PluginClassLoader pluginClassLoader = pluginLoader.getPluginClassLoader(); + pluginDescriptor.setPluginClassLoader(pluginClassLoader); + pluginClassLoaders.put(pluginId, pluginClassLoader); + } + + private void expandPluginArchive(String fileName) throws IOException { + File pluginArchiveFile = new File(pluginsDirectory, fileName); + long pluginArchiveDate = pluginArchiveFile.lastModified(); + String pluginName = fileName.substring(0, fileName.length() - 4); + File pluginDirectory = new File(pluginsDirectory, pluginName); + // check if exists directory or the '.zip' file is "newer" than directory + if (!pluginDirectory.exists() || pluginArchiveDate > pluginDirectory.lastModified()) { + LOG.debug("Expand plugin archive '" + pluginArchiveFile + "' in '" + pluginDirectory + "'"); + // create directorie for plugin + pluginDirectory.mkdirs(); + + // expand '.zip' file + Unzip unzip = new Unzip(); + unzip.setSource(pluginArchiveFile); + unzip.setDestination(pluginDirectory); + unzip.extract(); + } + } + + private Plugin getPluginInstance(PluginWrapper pluginWrapper, PluginLoader pluginLoader) + throws Exception { + String pluginClassName = pluginWrapper.getDescriptor().getPluginClass(); + + ClassLoader pluginClassLoader = pluginLoader.getPluginClassLoader(); + Class pluginClass = pluginClassLoader.loadClass(pluginClassName); + + // once we have the class, we can do some checks on it to ensure + // that it is a valid implementation of a plugin. + int modifiers = pluginClass.getModifiers(); + if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) + || (!Plugin.class.isAssignableFrom(pluginClass))) { + throw new PluginException("The plugin class '" + pluginClassName + + "' is not compatible."); + } + + // create the plugin instance + Constructor constructor = pluginClass.getConstructor(new Class[] { PluginWrapper.class }); + Plugin plugin = (Plugin) constructor.newInstance(new Object[] { pluginWrapper }); + + return plugin; + } + + private void resolvePlugins() { + resolveDependencies(); + } + + private void resolveDependencies() { + DependencyResolver dependencyResolver = new DependencyResolver(unresolvedPlugins); + resolvedPlugins = dependencyResolver.getSortedDependencies(); + for (Plugin plugin : resolvedPlugins) { + unresolvedPlugins.remove(plugin); + uberClassLoader.addLoader(plugin.getWrapper().getPluginClassLoader()); + } + } + +} diff --git a/pf4j/src/main/java/org/pf4j/DependencyResolver.java b/pf4j/src/main/java/org/pf4j/DependencyResolver.java new file mode 100644 index 0000000..2f5f601 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/DependencyResolver.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.util.ArrayList; +import java.util.List; + +import org.pf4j.util.DirectedGraph; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * @author Decebal Suiu + */ +class DependencyResolver { + + private static final Logger LOG = LoggerFactory.getLogger(DependencyResolver.class); + + private List plugins; + + public DependencyResolver(List plugins) { + this.plugins = plugins; + } + + /** + * Get the list of plugins in dependency sorted order. + */ + public List getSortedDependencies() { + DirectedGraph graph = new DirectedGraph(); + for (Plugin plugin : plugins) { + PluginDescriptor descriptor = plugin.getWrapper().getDescriptor(); + String pluginId = descriptor.getPluginId(); + List dependencies = descriptor.getDependencies(); + if (!dependencies.isEmpty()) { + for (String dependency : dependencies) { + graph.addEdge(pluginId, dependency); + } + } else { + graph.addVertex(pluginId); + } + } + + LOG.debug("Graph: " + graph); + List pluginsId = graph.reverseTopologicalSort(); + + if (pluginsId == null) { + LOG.error("Cyclic dependences !!!"); + return null; + } + + LOG.debug("Plugins order: " + pluginsId); + List sortedPlugins = new ArrayList(); + for (String pluginId : pluginsId) { + sortedPlugins.add(getPlugin(pluginId)); + } + + return sortedPlugins; + } + + private Plugin getPlugin(String pluginId) { + for (Plugin plugin : plugins) { + if (pluginId.equals(plugin.getWrapper().getDescriptor().getPluginId())) { + return plugin; + } + } + + return null; + } + +} diff --git a/pf4j/src/main/java/org/pf4j/Extension.java b/pf4j/src/main/java/org/pf4j/Extension.java new file mode 100644 index 0000000..6c499b6 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/Extension.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012 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 org.pf4j; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import net.java.sezpoz.Indexable; + +/** + * @author Decebal Suiu + */ +@Indexable +@Retention(RUNTIME) +@Target(TYPE) +@Documented +public @interface Extension { + + int ordinal() default 0; + +} diff --git a/pf4j/src/main/java/org/pf4j/ExtensionFinder.java b/pf4j/src/main/java/org/pf4j/ExtensionFinder.java new file mode 100644 index 0000000..5fc466d --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/ExtensionFinder.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.util.List; + +/** + * @author Decebal Suiu + */ +public interface ExtensionFinder { + + public List> find(Class type); + +} diff --git a/pf4j/src/main/java/org/pf4j/ExtensionPoint.java b/pf4j/src/main/java/org/pf4j/ExtensionPoint.java new file mode 100644 index 0000000..9113c1d --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/ExtensionPoint.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012 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 org.pf4j; + +/** + * @author Decebal Suiu + */ +public interface ExtensionPoint { + +} diff --git a/pf4j/src/main/java/org/pf4j/ExtensionWrapper.java b/pf4j/src/main/java/org/pf4j/ExtensionWrapper.java new file mode 100644 index 0000000..9ba320e --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/ExtensionWrapper.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012 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 org.pf4j; + +/** + * @author Decebal Suiu + */ +public class ExtensionWrapper implements Comparable> { + + private final T instance; + private final int ordinal; + + public ExtensionWrapper(T instance, int ordinal) { + this.instance = instance; + this.ordinal = ordinal; + } + + public T getInstance() { + return instance; + } + + public int getOrdinal() { + return ordinal; + } + + @Override + public int compareTo(ExtensionWrapper o) { + return (ordinal - o.getOrdinal()); + } + +} \ No newline at end of file diff --git a/pf4j/src/main/java/org/pf4j/Plugin.java b/pf4j/src/main/java/org/pf4j/Plugin.java new file mode 100644 index 0000000..bab55c1 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/Plugin.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012 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 org.pf4j; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class will be extended by all plugins and + * serve as the common class between a plugin and the application. + * + * @author Decebal Suiu + */ +public abstract class Plugin { + + /** + * Makes logging service available for descending classes. + */ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + /** + * Wrapper of the plugin. + */ + PluginWrapper wrapper; + + /** + * Constructor to be used by plugin manager for plugin instantiation. + * Your plugins have to provide constructor with this exact signature to + * be successfully loaded by manager. + */ + public Plugin(final PluginWrapper wrapper) { + if (wrapper == null) { + throw new IllegalArgumentException("Wrapper cannot be null"); + } + + this.wrapper = wrapper; + } + + /** + * Retrieves the wrapper of this plug-in. + */ + public final PluginWrapper getWrapper() { + return wrapper; + } + + /** + * Start method is called by the application when the plugin is loaded. + */ + public void start() throws PluginException { + } + + /** + * Stop method is called by the application when the plugin is unloaded. + */ + public void stop() throws PluginException { + } + +} diff --git a/pf4j/src/main/java/org/pf4j/PluginClassLoader.java b/pf4j/src/main/java/org/pf4j/PluginClassLoader.java new file mode 100644 index 0000000..884642f --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/PluginClassLoader.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; + +/** + * One instance of this class should be created by plugin manager for every available plug-in. + * + * @author Decebal Suiu + */ +class PluginClassLoader extends URLClassLoader { + + private static final String JAVA_PACKAGE_PREFIX = "java."; + private static final String JAVAX_PACKAGE_PREFIX = "javax."; + private static final String PLUGIN_PACKAGE_PREFIX = "org.pf4j."; + + private PluginManager pluginManager; + private PluginWrapper pluginWrapper; + + public PluginClassLoader(PluginManager pluginManager, PluginWrapper pluginWrapper, ClassLoader parent) { + super(new URL[0], parent); + + this.pluginManager = pluginManager; + this.pluginWrapper = pluginWrapper; + } + + @Override + public void addURL(URL url) { + super.addURL(url); + } + + @Override + public Class loadClass(String className) throws ClassNotFoundException { +// System.out.println(">>>" + className); + + // 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); + } + + // second check whether it's already been loaded + Class loadedClass = findLoadedClass(className); + if (loadedClass != null) { + return loadedClass; + } + + // nope, try to load locally + try { + return findClass(className); + } catch (ClassNotFoundException e) { + // try next step + } + + // if the class it's a part of the plugin engine use parent class loader + if (className.startsWith(PLUGIN_PACKAGE_PREFIX)) { + try { + return PluginClassLoader.class.getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + // try next step + } + } + + // look in dependencies + List dependencies = pluginWrapper.getDescriptor().getDependencies(); + for (String dependency : dependencies) { + PluginClassLoader classLoader = pluginManager.getPluginClassLoader(dependency); + try { + return classLoader.loadClass(className); + } catch (ClassNotFoundException e) { + // try next dependency + } + } + + // use the standard URLClassLoader (which follows normal parent delegation) + return super.loadClass(className); + } + +} diff --git a/pf4j/src/main/java/org/pf4j/PluginDescriptor.java b/pf4j/src/main/java/org/pf4j/PluginDescriptor.java new file mode 100644 index 0000000..8346e48 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/PluginDescriptor.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; + +/** + * A plugin descriptor contains information about a plug-in obtained + * from the manifest (META-INF) file. + * + * @author Decebal Suiu + */ +class PluginDescriptor { + + private String pluginId; + private String pluginClass; + private PluginVersion version; + private String provider; + private String pluginPath; + private List dependencies; + private PluginClassLoader pluginClassLoader; + + public PluginDescriptor() { + dependencies = new ArrayList(); + } + + /** + * Returns the unique identifier of this plugin. + */ + public String getPluginId() { + return pluginId; + } + + /** + * Returns the name of the class that implements Plugin interface. + */ + public String getPluginClass() { + return pluginClass; + } + + /** + * Returns the version of this plugin. + */ + public PluginVersion getVersion() { + return version; + } + + /** + * Returns the provider name of this plugin. + */ + public String getProvider() { + return provider; + } + + /** + * Returns the path of this plugin relative to plugins directory. + */ + public String getPluginPath() { + return pluginPath; + } + + /** + * Returns all dependencies declared by this plugin. + * Returns an empty array if this plugin does not declare any require. + */ + public List getDependencies() { + return dependencies; + } + + /** + * Returns the plugin class loader used to load classes and resources + * for this plug-in. The class loader can be used to directly access + * plug-in resources and classes. + */ + public PluginClassLoader getPluginClassLoader() { + return pluginClassLoader; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("pluginId", pluginId) + .append("pluginClass", pluginClass) + .append("version", version) + .append("provider", provider) + .append("pluginPath", pluginPath) + .append("dependencies", dependencies) + .toString(); + } + + void setPluginId(String pluginId) { + this.pluginId = pluginId; + } + + void setPluginClass(String pluginClassName) { + this.pluginClass = pluginClassName; + } + + void setPluginVersion(PluginVersion version) { + this.version = version; + } + + void setProvider(String provider) { + this.provider = provider; + } + + void setPluginPath(String pluginPath) { + this.pluginPath = pluginPath; + } + + void setDependencies(String dependencies) { + if (dependencies != null) { + this.dependencies = Arrays.asList(StringUtils.split(dependencies, ',')); + } else { + this.dependencies = Collections.emptyList(); + } + } + + void setPluginClassLoader(PluginClassLoader pluginClassLoader) { + this.pluginClassLoader = pluginClassLoader; + } + +} diff --git a/pf4j/src/main/java/org/pf4j/PluginDescriptorFinder.java b/pf4j/src/main/java/org/pf4j/PluginDescriptorFinder.java new file mode 100644 index 0000000..c59ec65 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/PluginDescriptorFinder.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.io.File; + +/** + * Find a plugin descriptor in a directory (plugin repository). + * You can find in manifest file @see DefaultPluginDescriptorFinder, + * xml file, properties file, java services (with ServiceLoader), etc. + * + * @author Decebal Suiu + */ +public interface PluginDescriptorFinder { + + public PluginDescriptor find(File pluginRepository) throws PluginException; + +} diff --git a/pf4j/src/main/java/org/pf4j/PluginException.java b/pf4j/src/main/java/org/pf4j/PluginException.java new file mode 100644 index 0000000..c341c70 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/PluginException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012 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 org.pf4j; + +/** + * An exception used to indicate that a plugin problem occurred. + * + * @author Decebal Suiu + */ +class PluginException extends Exception { + + private static final long serialVersionUID = 1L; + + public PluginException() { + super(); + } + + public PluginException(String message) { + super(message); + } + + public PluginException(Throwable cause) { + super(cause); + } + + public PluginException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/pf4j/src/main/java/org/pf4j/PluginLoader.java b/pf4j/src/main/java/org/pf4j/PluginLoader.java new file mode 100644 index 0000000..bd64d59 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/PluginLoader.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.io.File; +import java.io.FilenameFilter; +import java.net.MalformedURLException; +import java.util.Vector; + +import org.pf4j.util.DirectoryFilter; +import org.pf4j.util.JarFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Load all informations needed by a plugin. + * This means add all jar files from 'lib' directory, 'classes' + * to classpath. + * It's a class for only the internal use. + * + * @author Decebal Suiu + */ +class PluginLoader { + + private static final Logger LOG = LoggerFactory.getLogger(PluginLoader.class); + + /* + * The plugin repository. + */ + private File pluginRepository; + + /* + * The directory with '.class' files. + */ + private File classesDirectory; + + /* + * The directory with '.jar' files. + */ + private File libDirectory; + + private PluginClassLoader pluginClassLoader; + + public PluginLoader(PluginManager pluginManager, PluginWrapper pluginWrapper, File pluginRepository) { + this.pluginRepository = pluginRepository; + classesDirectory = new File(pluginRepository, "classes"); + libDirectory = new File(pluginRepository, "lib"); + ClassLoader parent = getClass().getClassLoader(); + pluginClassLoader = new PluginClassLoader(pluginManager, pluginWrapper, parent); + LOG.debug("Created class loader " + pluginClassLoader); + } + + public File getPluginRepository() { + return pluginRepository; + } + + public boolean load() { + return loadClassesAndJars(); + } + + public PluginClassLoader getPluginClassLoader() { + return pluginClassLoader; + } + + private boolean loadClassesAndJars() { + return loadClasses() && loadJars(); + } + + private void getJars(Vector v, File file) { + FilenameFilter jarFilter = new JarFilter(); + FilenameFilter directoryFilter = new DirectoryFilter(); + + if (file.exists() && file.isDirectory() && file.isAbsolute()) { + String[] jars = file.list(jarFilter); + for (int i = 0; (jars != null) && (i < jars.length); ++i) { + v.addElement(jars[i]); + } + + String[] directoryList = file.list(directoryFilter); + for (int i = 0; (directoryList != null) && (i < directoryList.length); ++i) { + File directory = new File(file, directoryList[i]); + getJars(v, directory); + } + } + } + + private boolean loadClasses() { + // make 'classesDirectory' absolute + classesDirectory = classesDirectory.getAbsoluteFile(); + + if (classesDirectory.exists() && classesDirectory.isDirectory()) { + LOG.debug("Found '" + classesDirectory.getPath() + "' directory"); + + try { + pluginClassLoader.addURL(classesDirectory.toURI().toURL()); + LOG.debug("Added '" + classesDirectory + "' to the class loader path"); + } catch (MalformedURLException e) { + e.printStackTrace(); + LOG.error(e.getMessage(), e); + return false; + } + } + + return true; + } + + /** + * Add all *.jar files from '/lib' directory. + */ + private boolean loadJars() { + // make 'jarDirectory' absolute + libDirectory = libDirectory.getAbsoluteFile(); + + Vector jars = new Vector(); + getJars(jars, libDirectory); + for (String jar : jars) { + File jarFile = new File(libDirectory, jar); + try { + pluginClassLoader.addURL(jarFile.toURI().toURL()); + LOG.debug("Added '" + jarFile + "' to the class loader path"); + } catch (MalformedURLException e) { + e.printStackTrace(); + LOG.error(e.getMessage(), e); + return false; + } + } + + return true; + } + +} diff --git a/pf4j/src/main/java/org/pf4j/PluginManager.java b/pf4j/src/main/java/org/pf4j/PluginManager.java new file mode 100644 index 0000000..cf0cff1 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/PluginManager.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.util.List; + +/** + * Provides the functionality for plugin management such as load, + * start and stop the plugins. + * + * @author Decebal Suiu + */ +public interface PluginManager { + + /** + * Retrieves all plugins. + */ + public List getPlugins(); + + /** + * Load plugins. + */ + public void loadPlugins(); + + /** + * Start all active plugins. + */ + public void startPlugins(); + + /** + * Stop all active plugins. + */ + public void stopPlugins(); + + public PluginClassLoader getPluginClassLoader(String pluginId); + + public List> getExtensions(Class type); + +} diff --git a/pf4j/src/main/java/org/pf4j/PluginVersion.java b/pf4j/src/main/java/org/pf4j/PluginVersion.java new file mode 100644 index 0000000..66267c9 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/PluginVersion.java @@ -0,0 +1,191 @@ +/* + * Copyright 2012 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 org.pf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +/** + * Represents the version of a Plugin and allows versions to be compared. + * Version identifiers have five components. + * + * 1. Major version. A non-negative integer. + * 2. Minor version. A non-negative integer. + * 3. Release version. A non-negative integer. + * 4. Build version. A non-negative integer. + * 5. Qualifier. A text string. + * + * This class is immutable. + * + * @author Decebal Suiu + */ +public class PluginVersion implements Comparable { + + private int major; + private int minor; + private int release; + private int build; + private String qualifier; + + private PluginVersion() { + } + + public PluginVersion(int major, int minor, int release) { + this.major = major; + this.minor = minor; + this.release = release; + } + + public PluginVersion(int major, int minor, int release, int build) { + this.major = major; + this.minor = minor; + this.release = release; + this.build = build; + } + + public PluginVersion(int major, int minor, int release, int build, String qualifier) { + this.major = major; + this.minor = minor; + this.release = release; + this.build = build; + this.qualifier = qualifier; + } + + public static PluginVersion createVersion(String version) { + if (version == null) { + return new PluginVersion(); + } + + PluginVersion v = new PluginVersion(); + + StringTokenizer st = new StringTokenizer(version, "."); + List tmp = new ArrayList(); + for (int i = 0; st.hasMoreTokens() && i < 4; i++) { + tmp.add(st.nextToken()); + } + + int n = tmp.size(); + switch (n) { + case 0 : + break; + case 1 : + v.major = Integer.parseInt(tmp.get(0)); + break; + case 2 : + v.major = Integer.parseInt(tmp.get(0)); + v.minor = Integer.parseInt(tmp.get(1)); + break; + case 3 : + v.major = Integer.parseInt(tmp.get(0)); + v.minor = Integer.parseInt(tmp.get(1)); + v.release = Integer.parseInt(tmp.get(2)); + break; + case 4 : + v.major = Integer.parseInt(tmp.get(0)); + v.minor = Integer.parseInt(tmp.get(1)); + v.release = Integer.parseInt(tmp.get(2)); + v.build = Integer.parseInt(tmp.get(3)); + break; + } + + return v; + } + + public int getMajor() { + return this.major; + } + + public int getMinor() { + return this.minor; + } + + public int getRelease() { + return this.release; + } + + public int getBuild() { + return this.build; + } + + public String getQualifier() { + return qualifier; + } + + public String toString() { + StringBuffer sb = new StringBuffer(50); + sb.append(major); + sb.append('.'); + sb.append(minor); + sb.append('.'); + sb.append(release); + sb.append('.'); + sb.append(build); + if (qualifier != null) { + sb.append(qualifier); + } + + return sb.toString(); + } + + public int compareTo(PluginVersion version) { + if (version.major > major) { + return 1; + } else if (version.major < major) { + return -1; + } + + if (version.minor > minor) { + return 1; + } else if (version.minor < minor) { + return -1; + } + + if (version.release > release) { + return 1; + } else if (version.release < release) { + return -1; + } + + if (version.build > build) { + return 1; + } else if (version.build < build) { + return -1; + } + + return 0; + } + + /* + private String extractQualifier(String token) { + StringTokenizer st = new StringTokenizer(token, "-"); + if (st.countTokens() == 2) { + return st. + } + } + */ + + // for test only + public static void main(String[] args) { + PluginVersion v = PluginVersion.createVersion("4.0.0.123"); + System.out.println(v.toString()); +// v = PluginVersion.createVersion("4.0.0.123-alpha"); +// System.out.println(v.toString()); + PluginVersion v1 = PluginVersion.createVersion("4.1.0"); + System.out.println(v1.toString()); + PluginVersion v2 = PluginVersion.createVersion("4.0.32"); + System.out.println(v2.toString()); + System.out.println(v1.compareTo(v2)); + } + +} diff --git a/pf4j/src/main/java/org/pf4j/PluginWrapper.java b/pf4j/src/main/java/org/pf4j/PluginWrapper.java new file mode 100644 index 0000000..46a0da1 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/PluginWrapper.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012 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 org.pf4j; + +/** + * A wrapper over plugin instance. + * + * @author Decebal Suiu + */ +public class PluginWrapper { + + PluginDescriptor descriptor; + String pluginPath; + PluginClassLoader pluginClassLoader; + + public PluginWrapper(PluginDescriptor descriptor) { + this.descriptor = descriptor; + } + + /** + * Returns the plugin descriptor. + */ + public PluginDescriptor getDescriptor() { + return descriptor; + } + + /** + * Returns the path of this plugin relative to plugins directory. + */ + public String getPluginPath() { + return pluginPath; + } + + /** + * Returns the plugin class loader used to load classes and resources + * for this plug-in. The class loader can be used to directly access + * plug-in resources and classes. + */ + public PluginClassLoader getPluginClassLoader() { + return pluginClassLoader; + } + + void setPluginPath(String pluginPath) { + this.pluginPath = pluginPath; + } + + void setPluginClassLoader(PluginClassLoader pluginClassLoader) { + this.pluginClassLoader = pluginClassLoader; + } + +} diff --git a/pf4j/src/main/java/org/pf4j/util/DirectedGraph.java b/pf4j/src/main/java/org/pf4j/util/DirectedGraph.java new file mode 100644 index 0000000..a5fa829 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/util/DirectedGraph.java @@ -0,0 +1,171 @@ +/* + * Copyright 2012 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 org.pf4j.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +/** + * @author Decebal Suiu + */ +public class DirectedGraph { + + /** + * The implementation here is basically an adjacency list, but instead + * of an array of lists, a Map is used to map each vertex to its list of + * adjacent vertices. + */ + private Map> neighbors = new HashMap>(); + + /** + * Add a vertex to the graph. Nothing happens if vertex is already in graph. + */ + public void addVertex(V vertex) { + if (neighbors.containsKey(vertex)) { + return; + } + neighbors.put(vertex, new ArrayList()); + } + + /** + * True if graph contains vertex. + */ + public boolean containsVertex(V vertex) { + return neighbors.containsKey(vertex); + } + + /** + * Add an edge to the graph; if either vertex does not exist, it's added. + * This implementation allows the creation of multi-edges and self-loops. + */ + public void addEdge(V from, V to) { + this.addVertex(from); + this.addVertex(to); + neighbors.get(from).add(to); + } + + /** + * Remove an edge from the graph. Nothing happens if no such edge. + * @throws IllegalArgumentException if either vertex doesn't exist. + */ + public void remove(V from, V to) { + if (!(this.containsVertex(from) && this.containsVertex(to))) { + throw new IllegalArgumentException("Nonexistent vertex"); + } + neighbors.get(from).remove(to); + } + + /** + * Report (as a Map) the out-degree of each vertex. + */ + public Map outDegree() { + Map result = new HashMap(); + for (V vertex : neighbors.keySet()) { + result.put(vertex, neighbors.get(vertex).size()); + } + + return result; + } + + /** + * Report (as a Map) the in-degree of each vertex. + */ + public Map inDegree() { + Map result = new HashMap(); + for (V vertex : neighbors.keySet()) { + result.put(vertex, 0); // all in-degrees are 0 + } + for (V from : neighbors.keySet()) { + for (V to : neighbors.get(from)) { + result.put(to, result.get(to) + 1); // increment in-degree + } + } + + return result; + } + + /** + * Report (as a List) the topological sort of the vertices; null for no such sort. + */ + public List topologicalSort() { + Map degree = inDegree(); + + // determine all vertices with zero in-degree + Stack zeroVertices = new Stack(); // stack as good as any here + for (V v : degree.keySet()) { + if (degree.get(v) == 0) { + zeroVertices.push(v); + } + } + + // determine the topological order + List result = new ArrayList(); + while (!zeroVertices.isEmpty()) { + V vertex = zeroVertices.pop(); // choose a vertex with zero in-degree + result.add(vertex); // vertex 'v' is next in topological order + // "remove" vertex 'v' by updating its neighbors + for (V neighbor : neighbors.get(vertex)) { + degree.put(neighbor, degree.get(neighbor) - 1); + // remember any vertices that now have zero in-degree + if (degree.get(neighbor) == 0) { + zeroVertices.push(neighbor); + } + } + } + + // check that we have used the entire graph (if not, there was a cycle) + if (result.size() != neighbors.size()) { + return null; + } + + return result; + } + + /** + * Report (as a List) the reverse topological sort of the vertices; null for no such sort. + */ + public List reverseTopologicalSort() { + List list = topologicalSort(); + if (list == null) { + return null; + } + Collections.reverse(list); + + return list; + } + + /** + * True if graph is a dag (directed acyclic graph). + */ + public boolean isDag () { + return topologicalSort() != null; + } + + /** + * String representation of graph. + */ + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + for (V vertex : neighbors.keySet()) { + sb.append("\n " + vertex + " -> " + neighbors.get(vertex)); + } + + return sb.toString(); + } + +} diff --git a/pf4j/src/main/java/org/pf4j/util/DirectoryFilter.java b/pf4j/src/main/java/org/pf4j/util/DirectoryFilter.java new file mode 100644 index 0000000..c6a236b --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/util/DirectoryFilter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012 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 org.pf4j.util; + +import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; + +/** + * @author Decebal Suiu + */ +public class DirectoryFilter implements FileFilter, FilenameFilter { + + /** + * Accepts any file ending in .jar. The case of the filename is ignored. + */ + public boolean accept(File file) { + return file.isDirectory(); + } + + public boolean accept(File dir, String name) { + return accept(new File(dir, name)); + } + +} diff --git a/pf4j/src/main/java/org/pf4j/util/ExtensionFilter.java b/pf4j/src/main/java/org/pf4j/util/ExtensionFilter.java new file mode 100644 index 0000000..1252a4e --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/util/ExtensionFilter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012 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 org.pf4j.util; + +import java.io.File; +import java.io.FilenameFilter; + +/** + * @author Decebal Suiu + */ +public class ExtensionFilter implements FilenameFilter { + + private String extension; + + public ExtensionFilter(String extension) { + this.extension = extension; + } + + /** + * Accepts any file ending in extension. The case of the filename is ignored. + */ + public boolean accept(File file) { + // perform a case insensitive check. + return file.getName().toUpperCase().endsWith(extension.toUpperCase()); + } + + public boolean accept(File dir, String name) { + return accept(new File(dir, name)); + } + +} diff --git a/pf4j/src/main/java/org/pf4j/util/JarFilter.java b/pf4j/src/main/java/org/pf4j/util/JarFilter.java new file mode 100644 index 0000000..09c322c --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/util/JarFilter.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 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 org.pf4j.util; + +/** + * File filter that accepts all files ending with .JAR. + * This filter is case insensitive. + * + * @author Decebal Suiu + */ +public class JarFilter extends ExtensionFilter { + + /** + * The extension that this filter will search for. + */ + private static final String JAR_EXTENSION = ".JAR"; + + public JarFilter() { + super(JAR_EXTENSION); + } + +} diff --git a/pf4j/src/main/java/org/pf4j/util/UberClassLoader.java b/pf4j/src/main/java/org/pf4j/util/UberClassLoader.java new file mode 100644 index 0000000..4ecfe2c --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/util/UberClassLoader.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012 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 org.pf4j.util; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A class loader that has multiple loaders and uses them for loading classes and resources. + * + * @author Decebal Suiu + */ +public class UberClassLoader extends ClassLoader { + + private Set loaders = new HashSet(); + + public void addLoader(ClassLoader loader) { + loaders.add(loader); + } + + @Override + public Class findClass(String name) throws ClassNotFoundException { + for (ClassLoader loader : loaders) { + try { + return loader.loadClass(name); + } catch (ClassNotFoundException e) { + // try next + } + } + + throw new ClassNotFoundException(name); + } + + @Override + public URL findResource(String name) { + for (ClassLoader loader : loaders) { + URL url = loader.getResource(name); + if (url != null) { + return url; + } + } + + return null; + } + + @Override + protected Enumeration findResources(String name) throws IOException { + List resources = new ArrayList(); + for (ClassLoader loader : loaders) { + resources.addAll(Collections.list(loader.getResources(name))); + } + + return Collections.enumeration(resources); + } + +} diff --git a/pf4j/src/main/java/org/pf4j/util/Unzip.java b/pf4j/src/main/java/org/pf4j/util/Unzip.java new file mode 100644 index 0000000..d5e6a04 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/util/Unzip.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012 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 org.pf4j.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class extracts the containt of the plugin archive into a directory. + * It's a class for only the internal use. + * + * @author Decebal Suiu + */ +public class Unzip { + + private static final Logger LOG = LoggerFactory.getLogger(Unzip.class); + + /** + * Holds the destination directory. + * File will be unzipped into the destination directory. + */ + private File destination; + + /** + * Holds path to zip file. + */ + private File source; + + public Unzip() { + } + + public Unzip(File source, File destination) { + this.source = source; + this.destination = destination; + } + + public void setSource(File source) { + this.source = source; + } + + public void setDestination(File destination) { + this.destination = destination; + } + + public void extract() throws IOException { + LOG.debug("Extract content of " + source + " to " + destination); + + // delete destination file if exists + removeDirectory(destination); + + ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(source)); + ZipEntry zipEntry = null; + + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + try { + File file = new File(destination, zipEntry.getName()); + + // create intermediary directories - sometimes zip don't add them + File dir = new File(file.getParent()); + dir.mkdirs(); + + if (zipEntry.isDirectory()) { + file.mkdirs(); + } else { + byte[] buffer = new byte[1024]; + int length = 0; + FileOutputStream fos = new FileOutputStream(file); + + while ((length = zipInputStream.read(buffer)) >= 0) { + fos.write(buffer, 0, length); + } + + fos.close(); + } + } catch (FileNotFoundException e) { + LOG.error("File '" + zipEntry.getName() + "' not found"); + } + } + + zipInputStream.close(); + } + + private boolean removeDirectory(File directory) { + if (!directory.exists()) { + return true; + } + + if (!directory.isDirectory()) { + return false; + } + + File[] files = directory.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + removeDirectory(file); + } else { + file.delete(); + } + } + + return directory.delete(); + } + +} diff --git a/pf4j/src/main/java/org/pf4j/util/ZipFilter.java b/pf4j/src/main/java/org/pf4j/util/ZipFilter.java new file mode 100644 index 0000000..4884e90 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/util/ZipFilter.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 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 org.pf4j.util; + +/** + * File filter that accepts all files ending with .ZIP. + * This filter is case insensitive. + * + * @author Decebal Suiu + */ +public class ZipFilter extends ExtensionFilter { + + /** + * The extension that this filter will search for. + */ + private static final String ZIP_EXTENSION = ".ZIP"; + + public ZipFilter() { + super(ZIP_EXTENSION); + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a8d45f9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,72 @@ + + + + 4.0.0 + org.pf4j + pom + 0.1-SNAPSHOT + pom + PF4J + Plugin Framework for Java + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + scm:git:https://github.com/decebals/pf4j.git + scm:git:https://github.com/decebals/pf4j.git + git@github.com/decebals/pf4j.git + + + + UTF-8 + 1.6.4 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + true + + + + + org.apache.maven.plugins + maven-release-plugin + 2.3.2 + + deploy + true + release-@{project.version} + + + + + maven-resources-plugin + 2.4.3 + + + + maven-jar-plugin + 2.3.1 + + + + + + pf4j + demo + + + diff --git a/run-demo.sh b/run-demo.sh new file mode 100755 index 0000000..bea8ba2 --- /dev/null +++ b/run-demo.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# +# This script creates and run the pf4j demo. +# + +# create artifacts using maven +mvn clean package + +# create demo-dist folder +rm -fr demo-dist +mkdir demo-dist +mkdir demo-dist/plugins + +# copy artifacts to demo-dist folder +cp -r demo/app/target/pf4j-demo-*/* demo-dist/ +cp demo/plugin1/target/pf4j-demo-plugin1-*.zip demo-dist/plugins/ +cp demo/plugin2/target/pf4j-demo-plugin2-*.zip demo-dist/plugins/ + +# run demo +cd demo-dist +java -jar pf4j-demo-app-*.jar +cd - +