@@ -20,10 +20,13 @@ import org.junit.jupiter.api.BeforeEach; | |||
import org.junit.jupiter.api.Test; | |||
import org.junit.jupiter.api.io.TempDir; | |||
import org.pf4j.plugin.PluginJar; | |||
import org.pf4j.plugin.TestExtension; | |||
import org.pf4j.plugin.TestExtensionPoint; | |||
import org.pf4j.plugin.TestPlugin; | |||
import java.io.IOException; | |||
import java.nio.file.Path; | |||
import java.util.List; | |||
import static org.junit.jupiter.api.Assertions.assertEquals; | |||
import static org.junit.jupiter.api.Assertions.assertFalse; | |||
@@ -42,6 +45,7 @@ public class JarPluginManagerTest { | |||
pluginJar = new PluginJar.Builder(pluginsPath.resolve("test-plugin.jar"), "test-plugin") | |||
.pluginClass(TestPlugin.class.getName()) | |||
.pluginVersion("1.2.3") | |||
.extension(TestExtension.class.getName()) | |||
.build(); | |||
pluginManager = new JarPluginManager(pluginsPath); | |||
@@ -53,6 +57,18 @@ public class JarPluginManagerTest { | |||
pluginManager = null; | |||
} | |||
@Test | |||
public void getExtensions() { | |||
pluginManager.loadPlugins(); | |||
pluginManager.startPlugins(); | |||
List<TestExtensionPoint> extensions = pluginManager.getExtensions(TestExtensionPoint.class); | |||
assertEquals(1, extensions.size()); | |||
String something = extensions.get(0).saySomething(); | |||
assertEquals(new TestExtension().saySomething(), something); | |||
} | |||
@Test | |||
public void unloadPlugin() throws Exception { | |||
pluginManager.loadPlugins(); |
@@ -0,0 +1,33 @@ | |||
/* | |||
* Copyright (C) 2012-present the original author or authors. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package org.pf4j.plugin; | |||
/** | |||
* Defines the interface for classes that know to supply class data for a class name. | |||
* The idea is to have the possibility to retrieve the data for a class from different sources: | |||
* <ul> | |||
* <li>Class path - the class is already loaded by the class loader</li> | |||
* <li>String - the string (the source code) is compiled dynamically via {@link javax.tools.JavaCompiler}</> | |||
* <li>Generate the source code programmatically using something like {@code https://github.com/square/javapoet}</li> | |||
* </ul> | |||
* | |||
* @author Decebal Suiu | |||
*/ | |||
public interface ClassDataProvider { | |||
byte[] getClassData(String className); | |||
} |
@@ -0,0 +1,55 @@ | |||
/* | |||
* Copyright (C) 2012-present the original author or authors. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package org.pf4j.plugin; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.OutputStream; | |||
/** | |||
* Get class data from the class path. | |||
* | |||
* @author Decebal Suiu | |||
*/ | |||
public class DefaultClassDataProvider implements ClassDataProvider { | |||
@Override | |||
public byte[] getClassData(String className) { | |||
String path = className.replace('.', '/') + ".class"; | |||
InputStream classDataStream = getClass().getClassLoader().getResourceAsStream(path); | |||
if (classDataStream == null) { | |||
throw new RuntimeException("Cannot find class data"); | |||
} | |||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { | |||
copyStream(classDataStream, outputStream); | |||
return outputStream.toByteArray(); | |||
} catch (IOException e) { | |||
throw new RuntimeException(e.getMessage(), e); | |||
} | |||
} | |||
private void copyStream(InputStream in, OutputStream out) throws IOException { | |||
byte[] buffer = new byte[1024]; | |||
int bytesRead; | |||
while ((bytesRead = in.read(buffer)) != -1) { | |||
out.write(buffer, 0, bytesRead); | |||
} | |||
} | |||
} |
@@ -26,4 +26,9 @@ public class FailTestExtension implements TestExtensionPoint { | |||
public FailTestExtension(String name) { | |||
} | |||
@Override | |||
public String saySomething() { | |||
return "I am a fail test extension"; | |||
} | |||
} |
@@ -17,18 +17,23 @@ package org.pf4j.plugin; | |||
import org.pf4j.ManifestPluginDescriptorFinder; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.File; | |||
import java.io.FileOutputStream; | |||
import java.io.IOException; | |||
import java.io.OutputStream; | |||
import java.io.PrintWriter; | |||
import java.nio.file.Path; | |||
import java.util.LinkedHashMap; | |||
import java.util.LinkedHashSet; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import java.util.jar.Attributes; | |||
import java.util.jar.JarEntry; | |||
import java.util.jar.JarOutputStream; | |||
import java.util.jar.Manifest; | |||
/** | |||
/** | |||
* Represents a plugin {@code jar} file. | |||
* The {@code MANIFEST.MF} file is created on the fly from the information supplied in {@link Builder}. | |||
* | |||
@@ -87,6 +92,8 @@ public class PluginJar { | |||
private String pluginClass; | |||
private String pluginVersion; | |||
private Map<String, String> manifestAttributes = new LinkedHashMap<>(); | |||
private Set<String> extensions = new LinkedHashSet<>(); | |||
private ClassDataProvider classDataProvider = new DefaultClassDataProvider(); | |||
public Builder(Path path, String pluginId) { | |||
this.path = path; | |||
@@ -125,13 +132,44 @@ public class PluginJar { | |||
return this; | |||
} | |||
public Builder extension(String extensionClassName) { | |||
extensions.add(extensionClassName); | |||
return this; | |||
} | |||
public Builder classDataProvider(ClassDataProvider classDataProvider) { | |||
this.classDataProvider = classDataProvider; | |||
return this; | |||
} | |||
public PluginJar build() throws IOException { | |||
createManifestFile(); | |||
Manifest manifest = createManifest(); | |||
try (OutputStream outputStream = new FileOutputStream(path.toFile())) { | |||
JarOutputStream jarOutputStream = new JarOutputStream(outputStream, manifest); | |||
if (!extensions.isEmpty()) { | |||
// add extensions.idx | |||
JarEntry jarEntry = new JarEntry("META-INF/extensions.idx"); | |||
jarOutputStream.putNextEntry(jarEntry); | |||
jarOutputStream.write(extensionsAsByteArray()); | |||
jarOutputStream.closeEntry(); | |||
// add extensions classes | |||
for (String extension : extensions) { | |||
String extensionPath = extension.replace('.', '/') + ".class"; | |||
JarEntry classEntry = new JarEntry(extensionPath); | |||
jarOutputStream.putNextEntry(classEntry); | |||
jarOutputStream.write(classDataProvider.getClassData(extension)); | |||
jarOutputStream.closeEntry(); | |||
} | |||
} | |||
jarOutputStream.close(); | |||
} | |||
return new PluginJar(this); | |||
} | |||
protected void createManifestFile() throws IOException { | |||
private Manifest createManifest() { | |||
Map<String, String> map = new LinkedHashMap<>(); | |||
map.put(ManifestPluginDescriptorFinder.PLUGIN_ID, pluginId); | |||
map.put(ManifestPluginDescriptorFinder.PLUGIN_VERSION, pluginVersion); | |||
@@ -142,9 +180,19 @@ public class PluginJar { | |||
map.putAll(manifestAttributes); | |||
} | |||
Manifest manifest = createManifest(map); | |||
JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(path.toFile()), manifest); | |||
outputStream.close(); | |||
return PluginJar.createManifest(map); | |||
} | |||
private byte[] extensionsAsByteArray() throws IOException { | |||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { | |||
PrintWriter writer = new PrintWriter(outputStream); | |||
for (String extension : extensions) { | |||
writer.println(extension); | |||
} | |||
writer.flush(); | |||
return outputStream.toByteArray(); | |||
} | |||
} | |||
} |
@@ -23,4 +23,9 @@ import org.pf4j.Extension; | |||
@Extension | |||
public class TestExtension implements TestExtensionPoint { | |||
@Override | |||
public String saySomething() { | |||
return "I am a test extension"; | |||
} | |||
} |
@@ -22,4 +22,6 @@ import org.pf4j.ExtensionPoint; | |||
*/ | |||
public interface TestExtensionPoint extends ExtensionPoint { | |||
String saySomething(); | |||
} |