--- /dev/null
+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
+<dependency>
+ <groupId>org.pf4j</groupId>
+ <artifactId>pf4j</artifactId>
+ <version>${pf4j.version}</version>
+</dependency>
+```
+
+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<ExtensionWrapper<Greeting>> greetings = pluginManager.getExtensions(Greeting.class);
+ for (ExtensionWrapper<Greeting> 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.
--- /dev/null
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <parent>
+ <groupId>org.pf4j.demo</groupId>
+ <artifactId>pom</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>pf4j-demo-api</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>jar</packaging>
+ <name>Demo Api</name>
+
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.pf4j</groupId>
+ <artifactId>pf4j</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
--- /dev/null
+/*
+ * 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();
+
+}
--- /dev/null
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <parent>
+ <groupId>org.pf4j.demo</groupId>
+ <artifactId>pom</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>pf4j-demo-app</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>jar</packaging>
+ <name>Demo App</name>
+
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <properties>
+ <main.class>org.pf4j.demo.Boot</main.class>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <version>2.3</version>
+ <configuration>
+ <descriptors>
+ <descriptor>
+ src/main/assembly/assembly.xml
+ </descriptor>
+ </descriptors>
+ <appendAssemblyId>false</appendAssemblyId>
+ </configuration>
+ <executions>
+ <execution>
+ <id>make-assembly</id>
+ <phase>package</phase>
+ <goals>
+ <goal>attached</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.3.1</version>
+ <configuration>
+ <archive>
+ <manifest>
+ <addClasspath>true</addClasspath>
+ <classpathPrefix>lib/</classpathPrefix>
+ <mainClass>${main.class}</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.pf4j</groupId>
+ <artifactId>pf4j</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.pf4j.demo</groupId>
+ <artifactId>pf4j-demo-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+</project>
--- /dev/null
+<!--
+ Describes the dist
+
+ @author Decebal Suiu
+ @version 1.0
+-->
+<assembly>
+ <id>plugin</id>
+ <formats>
+ <format>dir</format>
+ </formats>
+ <includeBaseDirectory>false</includeBaseDirectory>
+ <dependencySets>
+ <dependencySet>
+ <useProjectArtifact>false</useProjectArtifact>
+ <outputDirectory>lib</outputDirectory>
+ <includes>
+ <include>*:jar:*</include>
+ </includes>
+ </dependencySet>
+ </dependencySets>
+ <fileSets>
+ <fileSet>
+ <directory>${project.build.directory}</directory>
+ <outputDirectory></outputDirectory>
+ <includes>
+ <include>*.jar</include>
+ </includes>
+ </fileSet>
+ </fileSets>
+</assembly>
+
--- /dev/null
+/*
+ * 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<ExtensionWrapper<Greeting>> greetings = pluginManager.getExtensions(Greeting.class);
+ for (ExtensionWrapper<Greeting> 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));
+ }
+
+}
--- /dev/null
+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
--- /dev/null
+#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
--- /dev/null
+plugin.id=welcome-plugin
+plugin.class=org.pf4j.demo.welcome.WelcomePlugin
+plugin.version=0.0.1
+plugin.provider=Decebal Suiu
+plugin.dependencies=
--- /dev/null
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <parent>
+ <groupId>org.pf4j.demo</groupId>
+ <artifactId>pom</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>pf4j-demo-plugin1</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>jar</packaging>
+ <name>Demo Plugin #1</name>
+
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>properties-maven-plugin</artifactId>
+ <version>1.0-alpha-2</version>
+ <executions>
+ <execution>
+ <phase>initialize</phase>
+ <goals>
+ <goal>read-project-properties</goal>
+ </goals>
+ <configuration>
+ <files>
+ <file>plugin.properties</file>
+ </files>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <version>1.6</version>
+ <executions>
+ <execution>
+ <id>unzip jar file</id>
+ <phase>package</phase>
+ <configuration>
+ <target>
+ <unzip src="target/${artifactId}-${version}.${packaging}" dest="target/plugin-classes" />
+ </target>
+ </configuration>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <version>2.3</version>
+ <configuration>
+ <descriptors>
+ <descriptor>
+ src/main/assembly/assembly.xml
+ </descriptor>
+ </descriptors>
+ <appendAssemblyId>false</appendAssemblyId>
+ </configuration>
+ <executions>
+ <execution>
+ <id>make-assembly</id>
+ <phase>package</phase>
+ <goals>
+ <goal>attached</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Plugin-Id>${plugin.id}</Plugin-Id>
+ <Plugin-Class>${plugin.class}</Plugin-Class>
+ <Plugin-Version>${plugin.version}</Plugin-Version>
+ <Plugin-Provider>${plugin.provider}</Plugin-Provider>
+ <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.pf4j</groupId>
+ <artifactId>pf4j</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.pf4j.demo</groupId>
+ <artifactId>pf4j-demo-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
--- /dev/null
+<!--
+ Describes the plugin archive
+
+ @author Decebal Suiu
+ @version 1.0
+-->
+<assembly>
+ <id>plugin</id>
+ <formats>
+ <format>zip</format>
+ </formats>
+ <includeBaseDirectory>false</includeBaseDirectory>
+ <dependencySets>
+ <dependencySet>
+ <useProjectArtifact>false</useProjectArtifact>
+ <scope>runtime</scope>
+ <outputDirectory>lib</outputDirectory>
+ <includes>
+ <include>*:jar:*</include>
+ </includes>
+ </dependencySet>
+ </dependencySets>
+ <!--
+ <fileSets>
+ <fileSet>
+ <directory>target/classes</directory>
+ <outputDirectory>classes</outputDirectory>
+ </fileSet>
+ </fileSets>
+ -->
+ <fileSets>
+ <fileSet>
+ <directory>target/plugin-classes</directory>
+ <outputDirectory>classes</outputDirectory>
+ </fileSet>
+ </fileSets>
+</assembly>
--- /dev/null
+/*
+ * 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";
+ }
+
+ }
+
+}
--- /dev/null
+plugin.id=hello-plugin
+plugin.class=org.pf4j.demo.hello.HelloPlugin
+plugin.version=0.0.1
+plugin.provider=Decebal Suiu
+plugin.dependencies=
--- /dev/null
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <parent>
+ <groupId>org.pf4j.demo</groupId>
+ <artifactId>pom</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>pf4j-demo-plugin2</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>jar</packaging>
+ <name>Demo Plugin #2</name>
+
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>properties-maven-plugin</artifactId>
+ <version>1.0-alpha-2</version>
+ <executions>
+ <execution>
+ <phase>initialize</phase>
+ <goals>
+ <goal>read-project-properties</goal>
+ </goals>
+ <configuration>
+ <files>
+ <file>plugin.properties</file>
+ </files>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <version>1.6</version>
+ <executions>
+ <execution>
+ <id>unzip jar file</id>
+ <phase>package</phase>
+ <configuration>
+ <target>
+ <unzip src="target/${artifactId}-${version}.${packaging}" dest="target/plugin-classes" />
+ </target>
+ </configuration>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <version>2.3</version>
+ <configuration>
+ <appendAssemblyId>false</appendAssemblyId>
+ <descriptors>
+ <descriptor>
+ src/main/assembly/assembly.xml
+ </descriptor>
+ </descriptors>
+ <archive>
+ <manifestEntries>
+ <Plugin-Id>${plugin.id}</Plugin-Id>
+ <Plugin-Class>${plugin.class}</Plugin-Class>
+ <Plugin-Version>${plugin.version}</Plugin-Version>
+ <Plugin-Provider>${plugin.provider}</Plugin-Provider>
+ <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ <executions>
+ <execution>
+ <id>make-assembly</id>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Plugin-Id>${plugin.id}</Plugin-Id>
+ <Plugin-Class>${plugin.class}</Plugin-Class>
+ <Plugin-Version>${plugin.version}</Plugin-Version>
+ <Plugin-Provider>${plugin.provider}</Plugin-Provider>
+ <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.pf4j</groupId>
+ <artifactId>pf4j</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.pf4j.demo</groupId>
+ <artifactId>pf4j-demo-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
--- /dev/null
+<!--
+ Describes the plugin archive
+
+ @author Decebal Suiu
+ @version 1.0
+-->
+<assembly>
+ <id>plugin</id>
+ <formats>
+ <format>zip</format>
+ </formats>
+ <includeBaseDirectory>false</includeBaseDirectory>
+ <dependencySets>
+ <dependencySet>
+ <useProjectArtifact>false</useProjectArtifact>
+ <scope>runtime</scope>
+ <outputDirectory>lib</outputDirectory>
+ <includes>
+ <include>*:jar:*</include>
+ </includes>
+ </dependencySet>
+ </dependencySets>
+ <!--
+ <fileSets>
+ <fileSet>
+ <directory>target/classes</directory>
+ <outputDirectory>classes</outputDirectory>
+ </fileSet>
+ </fileSets>
+ -->
+ <fileSets>
+ <fileSet>
+ <directory>target/plugin-classes</directory>
+ <outputDirectory>classes</outputDirectory>
+ </fileSet>
+ </fileSets>
+</assembly>
--- /dev/null
+/*
+ * 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";
+ }
+
+ }
+
+}
--- /dev/null
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <parent>
+ <groupId>org.pf4j</groupId>
+ <artifactId>pom</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.pf4j.demo</groupId>
+ <artifactId>pom</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>pom</packaging>
+ <name>PF4J Demo</name>
+
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <build>
+ <resources>
+ <resource>
+ <filtering>false</filtering>
+ <directory>src/main/java</directory>
+ <excludes>
+ <exclude>**/*.java</exclude>
+ </excludes>
+ </resource>
+ <resource>
+ <directory>src/main/resources</directory>
+ </resource>
+ </resources>
+
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ <optimize>true</optimize>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <modules>
+ <module>app</module>
+ <module>api</module>
+ <module>plugin1</module>
+ <module>plugin2</module>
+ </modules>
+
+</project>
--- /dev/null
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <parent>
+ <groupId>org.pf4j</groupId>
+ <artifactId>pom</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>pf4j</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>jar</packaging>
+ <name>PF4J Library</name>
+ <description>Plugin Framework for Java</description>
+
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <scm>
+ <connection>scm:git:https://github.com/decebals/pf4j.git</connection>
+ <developerConnection>scm:git:https://github.com/decebals/pf4j.git</developerConnection>
+ <url>git@github.com/decebals/pf4j.git</url>
+ </scm>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <slf4j.version>1.6.4</slf4j.version>
+ </properties>
+
+ <build>
+ <resources>
+ <resource>
+ <filtering>false</filtering>
+ <directory>src/main/java</directory>
+ <excludes>
+ <exclude>**/*.java</exclude>
+ </excludes>
+ </resource>
+ <resource>
+ <directory>src/main/resources</directory>
+ </resource>
+ </resources>
+
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ <optimize>true</optimize>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-war-plugin</artifactId>
+ <version>2.1.1</version>
+ <configuration>
+ <warName>root</warName>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-release-plugin</artifactId>
+ <version>2.3</version>
+ <configuration>
+ <goals>deploy</goals>
+ <autoVersionSubmodules>true</autoVersionSubmodules>
+ <tagNameFormat>release-@{project.version}</tagNameFormat>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>commons-lang</groupId>
+ <artifactId>commons-lang</artifactId>
+ <version>2.4</version>
+ </dependency>
+
+ <dependency>
+ <groupId>net.java.sezpoz</groupId>
+ <artifactId>sezpoz</artifactId>
+ <version>1.9</version>
+ </dependency>
+
+ <!-- Logs -->
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <version>1.2.16</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>jul-to-slf4j</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+/*
+ * 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<IndexItem<Extension, Object>> indices;
+ private ClassLoader classLoader;
+
+ public DefaultExtensionFinder(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ }
+
+ @Override
+ public <T> List<ExtensionWrapper<T>> find(Class<T> type) {
+ LOG.debug("Find extensions for " + type);
+ List<ExtensionWrapper<T>> result = new ArrayList<ExtensionWrapper<T>>();
+ getIndices();
+// System.out.println("indices = "+ indices);
+ for (IndexItem<Extension, Object> 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<T>(type.cast(instance), item.annotation().ordinal()));
+ }
+ }
+ } catch (InstantiationException e) {
+ LOG.error(e.getMessage(), e);
+ }
+ }
+
+ return result;
+ }
+
+ private List<IndexItem<Extension, Object>> getIndices() {
+ if (indices == null) {
+ indices = new ArrayList<IndexItem<Extension, Object>>();
+ Iterator<IndexItem<Extension, Object>> it = Index.load(Extension.class, Object.class, classLoader).iterator();
+ while (it.hasNext()) {
+ indices.add(it.next());
+ }
+ }
+
+ return indices;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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<String, Plugin> plugins;
+
+ /**
+ * A map of plugin class loaders (he key is the 'pluginId').
+ */
+ private Map<String, PluginClassLoader> pluginClassLoaders;
+
+ /**
+ * A relation between 'pluginPath' and 'pluginId'
+ */
+ private Map<String, String> pathToIdMap;
+
+ /**
+ * A list with unresolved plugins (unresolved dependency).
+ */
+ private List<Plugin> unresolvedPlugins;
+
+ /**
+ * A list with resolved plugins (resolved dependency).
+ */
+ private List<Plugin> resolvedPlugins;
+
+ /**
+ * A list with disabled plugins.
+ */
+ private List<Plugin> 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<String, Plugin>();
+ pluginClassLoaders = new HashMap<String, PluginClassLoader>();
+ pathToIdMap = new HashMap<String, String>();
+ unresolvedPlugins = new ArrayList<Plugin>();
+ resolvedPlugins = new ArrayList<Plugin>();
+ disabledPlugins = new ArrayList<Plugin>();
+ pluginDescriptorFinder = new DefaultPluginDescriptorFinder();
+ uberClassLoader = new UberClassLoader();
+ extensionFinder = new DefaultExtensionFinder(uberClassLoader);
+ }
+
+ /**
+ * Retrieves all active plugins.
+ */
+ public List<Plugin> getPlugins() {
+ return new ArrayList<Plugin>(plugins.values());
+ }
+
+ public List<Plugin> getResolvedPlugins() {
+ return resolvedPlugins;
+ }
+
+ public Plugin getPlugin(String pluginId) {
+ return plugins.get(pluginId);
+ }
+
+ public List<Plugin> getUnresolvedPlugins() {
+ return unresolvedPlugins;
+ }
+
+ public List<Plugin> getDisabledPlugins() {
+ return disabledPlugins;
+ }
+
+ /**
+ * Start all active plugins.
+ */
+ public void startPlugins() {
+ List<Plugin> 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<Plugin> 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 <T> List<ExtensionWrapper<T>> getExtensions(Class<T> 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());
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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<Plugin> plugins;
+
+ public DependencyResolver(List<Plugin> plugins) {
+ this.plugins = plugins;
+ }
+
+ /**
+ * Get the list of plugins in dependency sorted order.
+ */
+ public List<Plugin> getSortedDependencies() {
+ DirectedGraph<String> graph = new DirectedGraph<String>();
+ for (Plugin plugin : plugins) {
+ PluginDescriptor descriptor = plugin.getWrapper().getDescriptor();
+ String pluginId = descriptor.getPluginId();
+ List<String> dependencies = descriptor.getDependencies();
+ if (!dependencies.isEmpty()) {
+ for (String dependency : dependencies) {
+ graph.addEdge(pluginId, dependency);
+ }
+ } else {
+ graph.addVertex(pluginId);
+ }
+ }
+
+ LOG.debug("Graph: " + graph);
+ List<String> pluginsId = graph.reverseTopologicalSort();
+
+ if (pluginsId == null) {
+ LOG.error("Cyclic dependences !!!");
+ return null;
+ }
+
+ LOG.debug("Plugins order: " + pluginsId);
+ List<Plugin> sortedPlugins = new ArrayList<Plugin>();
+ 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+
+}
--- /dev/null
+/*
+ * 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 <T> List<ExtensionWrapper<T>> find(Class<T> type);
+
+}
--- /dev/null
+/*
+ * 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 {
+
+}
--- /dev/null
+/*
+ * 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<T> implements Comparable<ExtensionWrapper<T>> {
+
+ 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<T> o) {
+ return (ordinal - o.getOrdinal());
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/*
+ * 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 {
+ }
+
+}
--- /dev/null
+/*
+ * 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<String> 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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<String> dependencies;
+ private PluginClassLoader pluginClassLoader;
+
+ public PluginDescriptor() {
+ dependencies = new ArrayList<String>();
+ }
+
+ /**
+ * 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<String> 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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<String> 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<String> jars = new Vector<String>();
+ 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;
+ }
+
+}
--- /dev/null
+/*
+ * 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<Plugin> 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 <T> List<ExtensionWrapper<T>> getExtensions(Class<T> type);
+
+}
--- /dev/null
+/*
+ * 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<PluginVersion> {
+
+ 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<String> tmp = new ArrayList<String>();
+ 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));
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
--- /dev/null
+/*\r
+ * Copyright 2012 Decebal Suiu\r
+ * \r
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with\r
+ * the License. You may obtain a copy of the License in the LICENSE file, or at:\r
+ * \r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ * \r
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on\r
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the\r
+ * specific language governing permissions and limitations under the License.\r
+ */\r
+package org.pf4j.util;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Collections;\r
+import java.util.HashMap;\r
+import java.util.List;\r
+import java.util.Map;\r
+import java.util.Stack;\r
+\r
+/**\r
+ * @author Decebal Suiu\r
+ */\r
+public class DirectedGraph<V> {\r
+\r
+ /**\r
+ * The implementation here is basically an adjacency list, but instead\r
+ * of an array of lists, a Map is used to map each vertex to its list of\r
+ * adjacent vertices.\r
+ */\r
+ private Map<V, List<V>> neighbors = new HashMap<V, List<V>>();\r
+\r
+ /**\r
+ * Add a vertex to the graph. Nothing happens if vertex is already in graph.\r
+ */\r
+ public void addVertex(V vertex) {\r
+ if (neighbors.containsKey(vertex)) {\r
+ return;\r
+ }\r
+ neighbors.put(vertex, new ArrayList<V>());\r
+ }\r
+\r
+ /**\r
+ * True if graph contains vertex.\r
+ */\r
+ public boolean containsVertex(V vertex) {\r
+ return neighbors.containsKey(vertex);\r
+ }\r
+\r
+ /**\r
+ * Add an edge to the graph; if either vertex does not exist, it's added.\r
+ * This implementation allows the creation of multi-edges and self-loops.\r
+ */\r
+ public void addEdge(V from, V to) {\r
+ this.addVertex(from);\r
+ this.addVertex(to);\r
+ neighbors.get(from).add(to);\r
+ }\r
+\r
+ /**\r
+ * Remove an edge from the graph. Nothing happens if no such edge.\r
+ * @throws IllegalArgumentException if either vertex doesn't exist.\r
+ */\r
+ public void remove(V from, V to) {\r
+ if (!(this.containsVertex(from) && this.containsVertex(to))) {\r
+ throw new IllegalArgumentException("Nonexistent vertex");\r
+ }\r
+ neighbors.get(from).remove(to);\r
+ }\r
+\r
+ /**\r
+ * Report (as a Map) the out-degree of each vertex.\r
+ */\r
+ public Map<V, Integer> outDegree() {\r
+ Map<V, Integer> result = new HashMap<V, Integer>();\r
+ for (V vertex : neighbors.keySet()) {\r
+ result.put(vertex, neighbors.get(vertex).size());\r
+ }\r
+\r
+ return result;\r
+ }\r
+\r
+ /**\r
+ * Report (as a Map) the in-degree of each vertex.\r
+ */\r
+ public Map<V,Integer> inDegree() {\r
+ Map<V, Integer> result = new HashMap<V, Integer>();\r
+ for (V vertex : neighbors.keySet()) {\r
+ result.put(vertex, 0); // all in-degrees are 0\r
+ }\r
+ for (V from : neighbors.keySet()) {\r
+ for (V to : neighbors.get(from)) {\r
+ result.put(to, result.get(to) + 1); // increment in-degree\r
+ }\r
+ }\r
+\r
+ return result;\r
+ }\r
+\r
+ /**\r
+ * Report (as a List) the topological sort of the vertices; null for no such sort.\r
+ */\r
+ public List<V> topologicalSort() {\r
+ Map<V, Integer> degree = inDegree();\r
+\r
+ // determine all vertices with zero in-degree\r
+ Stack<V> zeroVertices = new Stack<V>(); // stack as good as any here\r
+ for (V v : degree.keySet()) {\r
+ if (degree.get(v) == 0) {\r
+ zeroVertices.push(v);\r
+ }\r
+ }\r
+\r
+ // determine the topological order\r
+ List<V> result = new ArrayList<V>();\r
+ while (!zeroVertices.isEmpty()) {\r
+ V vertex = zeroVertices.pop(); // choose a vertex with zero in-degree\r
+ result.add(vertex); // vertex 'v' is next in topological order\r
+ // "remove" vertex 'v' by updating its neighbors\r
+ for (V neighbor : neighbors.get(vertex)) {\r
+ degree.put(neighbor, degree.get(neighbor) - 1);\r
+ // remember any vertices that now have zero in-degree\r
+ if (degree.get(neighbor) == 0) {\r
+ zeroVertices.push(neighbor);\r
+ }\r
+ }\r
+ }\r
+\r
+ // check that we have used the entire graph (if not, there was a cycle)\r
+ if (result.size() != neighbors.size()) {\r
+ return null;\r
+ }\r
+\r
+ return result;\r
+ }\r
+\r
+ /**\r
+ * Report (as a List) the reverse topological sort of the vertices; null for no such sort.\r
+ */\r
+ public List<V> reverseTopologicalSort() {\r
+ List<V> list = topologicalSort();\r
+ if (list == null) {\r
+ return null;\r
+ }\r
+ Collections.reverse(list);\r
+\r
+ return list;\r
+ }\r
+\r
+ /**\r
+ * True if graph is a dag (directed acyclic graph).\r
+ */\r
+ public boolean isDag () {\r
+ return topologicalSort() != null;\r
+ }\r
+\r
+ /**\r
+ * String representation of graph.\r
+ */\r
+ @Override\r
+ public String toString() {\r
+ StringBuffer sb = new StringBuffer();\r
+ for (V vertex : neighbors.keySet()) {\r
+ sb.append("\n " + vertex + " -> " + neighbors.get(vertex));\r
+ }\r
+\r
+ return sb.toString();\r
+ }\r
+\r
+}\r
--- /dev/null
+/*
+ * 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));
+ }
+
+}
--- /dev/null
+/*
+ * 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));
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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<ClassLoader> loaders = new HashSet<ClassLoader>();
+
+ 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<URL> findResources(String name) throws IOException {
+ List<URL> resources = new ArrayList<URL>();
+ for (ClassLoader loader : loaders) {
+ resources.addAll(Collections.list(loader.getResources(name)));
+ }
+
+ return Collections.enumeration(resources);
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.pf4j</groupId>
+ <artifactId>pom</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ <packaging>pom</packaging>
+ <name>PF4J</name>
+ <description>Plugin Framework for Java</description>
+
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <scm>
+ <connection>scm:git:https://github.com/decebals/pf4j.git</connection>
+ <developerConnection>scm:git:https://github.com/decebals/pf4j.git</developerConnection>
+ <url>git@github.com/decebals/pf4j.git</url>
+ </scm>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <slf4j.version>1.6.4</slf4j.version>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ <optimize>true</optimize>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-release-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <goals>deploy</goals>
+ <autoVersionSubmodules>true</autoVersionSubmodules>
+ <tagNameFormat>release-@{project.version}</tagNameFormat>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-resources-plugin</artifactId>
+ <version>2.4.3</version>
+ </plugin>
+
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.3.1</version>
+ </plugin>
+ </plugins>
+ </build>
+
+ <modules>
+ <module>pf4j</module>
+ <module>demo</module>
+ </modules>
+
+</project>
--- /dev/null
+#!/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 -
+