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;
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);
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();
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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) {
}
+ @Override
+ public String saySomething() {
+ return "I am a fail test extension";
+ }
+
}
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}.
*
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;
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);
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
public class TestExtension implements TestExtensionPoint {
+ @Override
+ public String saySomething() {
+ return "I am a test extension";
+ }
+
}
*/
public interface TestExtensionPoint extends ExtensionPoint {
+ String saySomething();
+
}