import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||
import org.junit.jupiter.api.io.TempDir; | import org.junit.jupiter.api.io.TempDir; | ||||
import org.pf4j.plugin.PluginJar; | import org.pf4j.plugin.PluginJar; | ||||
import org.pf4j.plugin.TestExtension; | |||||
import org.pf4j.plugin.TestExtensionPoint; | |||||
import org.pf4j.plugin.TestPlugin; | import org.pf4j.plugin.TestPlugin; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.nio.file.Path; | import java.nio.file.Path; | ||||
import java.util.List; | |||||
import static org.junit.jupiter.api.Assertions.assertEquals; | import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
import static org.junit.jupiter.api.Assertions.assertFalse; | import static org.junit.jupiter.api.Assertions.assertFalse; | ||||
pluginJar = new PluginJar.Builder(pluginsPath.resolve("test-plugin.jar"), "test-plugin") | pluginJar = new PluginJar.Builder(pluginsPath.resolve("test-plugin.jar"), "test-plugin") | ||||
.pluginClass(TestPlugin.class.getName()) | .pluginClass(TestPlugin.class.getName()) | ||||
.pluginVersion("1.2.3") | .pluginVersion("1.2.3") | ||||
.extension(TestExtension.class.getName()) | |||||
.build(); | .build(); | ||||
pluginManager = new JarPluginManager(pluginsPath); | pluginManager = new JarPluginManager(pluginsPath); | ||||
pluginManager = null; | 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 | @Test | ||||
public void unloadPlugin() throws Exception { | public void unloadPlugin() throws Exception { | ||||
pluginManager.loadPlugins(); | pluginManager.loadPlugins(); |
/* | |||||
* 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); | |||||
} |
/* | |||||
* 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); | |||||
} | |||||
} | |||||
} |
public FailTestExtension(String name) { | public FailTestExtension(String name) { | ||||
} | } | ||||
@Override | |||||
public String saySomething() { | |||||
return "I am a fail test extension"; | |||||
} | |||||
} | } |
import org.pf4j.ManifestPluginDescriptorFinder; | import org.pf4j.ManifestPluginDescriptorFinder; | ||||
import java.io.ByteArrayOutputStream; | |||||
import java.io.File; | import java.io.File; | ||||
import java.io.FileOutputStream; | import java.io.FileOutputStream; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.OutputStream; | |||||
import java.io.PrintWriter; | |||||
import java.nio.file.Path; | import java.nio.file.Path; | ||||
import java.util.LinkedHashMap; | import java.util.LinkedHashMap; | ||||
import java.util.LinkedHashSet; | |||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Set; | |||||
import java.util.jar.Attributes; | import java.util.jar.Attributes; | ||||
import java.util.jar.JarEntry; | |||||
import java.util.jar.JarOutputStream; | import java.util.jar.JarOutputStream; | ||||
import java.util.jar.Manifest; | import java.util.jar.Manifest; | ||||
/** | /** | ||||
/** | |||||
* Represents a plugin {@code jar} file. | * Represents a plugin {@code jar} file. | ||||
* The {@code MANIFEST.MF} file is created on the fly from the information supplied in {@link Builder}. | * The {@code MANIFEST.MF} file is created on the fly from the information supplied in {@link Builder}. | ||||
* | * | ||||
private String pluginClass; | private String pluginClass; | ||||
private String pluginVersion; | private String pluginVersion; | ||||
private Map<String, String> manifestAttributes = new LinkedHashMap<>(); | private Map<String, String> manifestAttributes = new LinkedHashMap<>(); | ||||
private Set<String> extensions = new LinkedHashSet<>(); | |||||
private ClassDataProvider classDataProvider = new DefaultClassDataProvider(); | |||||
public Builder(Path path, String pluginId) { | public Builder(Path path, String pluginId) { | ||||
this.path = path; | this.path = path; | ||||
return this; | 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 { | 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); | return new PluginJar(this); | ||||
} | } | ||||
protected void createManifestFile() throws IOException { | |||||
private Manifest createManifest() { | |||||
Map<String, String> map = new LinkedHashMap<>(); | Map<String, String> map = new LinkedHashMap<>(); | ||||
map.put(ManifestPluginDescriptorFinder.PLUGIN_ID, pluginId); | map.put(ManifestPluginDescriptorFinder.PLUGIN_ID, pluginId); | ||||
map.put(ManifestPluginDescriptorFinder.PLUGIN_VERSION, pluginVersion); | map.put(ManifestPluginDescriptorFinder.PLUGIN_VERSION, pluginVersion); | ||||
map.putAll(manifestAttributes); | 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(); | |||||
} | |||||
} | } | ||||
} | } |
@Extension | @Extension | ||||
public class TestExtension implements TestExtensionPoint { | public class TestExtension implements TestExtensionPoint { | ||||
@Override | |||||
public String saySomething() { | |||||
return "I am a test extension"; | |||||
} | |||||
} | } |
*/ | */ | ||||
public interface TestExtensionPoint extends ExtensionPoint { | public interface TestExtensionPoint extends ExtensionPoint { | ||||
String saySomething(); | |||||
} | } |