Browse Source

Support multiple plugin root directories (#404)

pull/412/head
rreich 4 years ago committed by GitHub
parent
commit
f6ede83be3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 65
      pf4j/src/main/java/org/pf4j/AbstractPluginManager.java
  2. 46
      pf4j/src/main/java/org/pf4j/BasePluginRepository.java
  3. 21
      pf4j/src/main/java/org/pf4j/DefaultPluginManager.java
  4. 25
      pf4j/src/main/java/org/pf4j/DefaultPluginRepository.java
  5. 10
      pf4j/src/main/java/org/pf4j/DevelopmentPluginRepository.java
  6. 8
      pf4j/src/main/java/org/pf4j/JarPluginManager.java
  7. 10
      pf4j/src/main/java/org/pf4j/JarPluginRepository.java
  8. 12
      pf4j/src/main/java/org/pf4j/PluginManager.java
  9. 4
      pf4j/src/main/java/org/pf4j/ZipPluginManager.java
  10. 59
      pf4j/src/test/java/org/pf4j/DefaultPluginRepositoryTest.java
  11. 75
      pf4j/src/test/java/org/pf4j/LoadPluginsFromMultipleRootsTest.java
  12. 6
      pf4j/src/test/java/org/pf4j/LoadPluginsTest.java

65
pf4j/src/main/java/org/pf4j/AbstractPluginManager.java

@ -25,12 +25,14 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* This class implements the boilerplate plugin code that any {@link PluginManager}
@ -51,7 +53,7 @@ public abstract class AbstractPluginManager implements PluginManager {
public static final String DEFAULT_PLUGINS_DIR = "plugins";
public static final String DEVELOPMENT_PLUGINS_DIR = "../plugins";
protected Path pluginsRoot;
protected final List<Path> pluginsRoots = new ArrayList<>();
protected ExtensionFinder extensionFinder;
@ -109,19 +111,28 @@ public abstract class AbstractPluginManager implements PluginManager {
protected VersionManager versionManager;
/**
* The plugins root is supplied by {@code System.getProperty("pf4j.pluginsDir", "plugins")}.
* The plugins roots are supplied as comma-separated list by {@code System.getProperty("pf4j.pluginsDir", "plugins")}.
*/
public AbstractPluginManager() {
initialize();
}
/**
* Constructs {@code AbstractPluginManager} with the given plugins root.
* Constructs {@code AbstractPluginManager} with the given plugins roots.
*
* @param pluginsRoot the root to search for plugins
* @param pluginsRoots the roots to search for plugins
*/
public AbstractPluginManager(Path pluginsRoot) {
this.pluginsRoot = pluginsRoot;
public AbstractPluginManager(Path... pluginsRoots) {
this(Arrays.asList(pluginsRoots));
}
/**
* Constructs {@code AbstractPluginManager} with the given plugins roots.
*
* @param pluginsRoots the roots to search for plugins
*/
public AbstractPluginManager(List<Path> pluginsRoots) {
this.pluginsRoots.addAll(pluginsRoots);
initialize();
}
@ -200,12 +211,17 @@ public abstract class AbstractPluginManager implements PluginManager {
*/
@Override
public void loadPlugins() {
log.debug("Lookup plugins in '{}'", pluginsRoot);
// check for plugins root
if (Files.notExists(pluginsRoot) || !Files.isDirectory(pluginsRoot)) {
log.warn("No '{}' root", pluginsRoot);
log.debug("Lookup plugins in '{}'", pluginsRoots);
// check for plugins roots
if (pluginsRoots.isEmpty()) {
log.warn("No plugins roots configured");
return;
}
pluginsRoots.forEach(path -> {
if (Files.notExists(path) || !Files.isDirectory(path)) {
log.warn("No '{}' root", path);
}
});
// get all plugin paths from repository
List<Path> pluginPaths = pluginRepository.getPluginPaths();
@ -599,8 +615,15 @@ public abstract class AbstractPluginManager implements PluginManager {
return pluginLoader;
}
@Override
public Path getPluginsRoot() {
return pluginsRoot;
return pluginsRoots.stream()
.findFirst()
.orElseThrow(() -> new IllegalStateException("pluginsRoots have not been initialized, yet."));
}
public List<Path> getPluginsRoots() {
return Collections.unmodifiableList(pluginsRoots);
}
@Override
@ -687,8 +710,8 @@ public abstract class AbstractPluginManager implements PluginManager {
pluginStateListeners = new ArrayList<>();
if (pluginsRoot == null) {
pluginsRoot = createPluginsRoot();
if (pluginsRoots.isEmpty()) {
pluginsRoots.addAll(createPluginsRoot());
}
pluginRepository = createPluginRepository();
@ -704,20 +727,24 @@ public abstract class AbstractPluginManager implements PluginManager {
}
/**
* Add the possibility to override the plugins root.
* If a {@link #PLUGINS_DIR_PROPERTY_NAME} system property is defined than this method returns that root.
* Add the possibility to override the plugins roots.
* If a {@link #PLUGINS_DIR_PROPERTY_NAME} system property is defined than this method returns that roots.
* If {@link #getRuntimeMode()} returns {@link RuntimeMode#DEVELOPMENT} than {@link #DEVELOPMENT_PLUGINS_DIR}
* is returned else this method returns {@link #DEFAULT_PLUGINS_DIR}.
*
* @return the plugins root
*/
protected Path createPluginsRoot() {
protected List<Path> createPluginsRoot() {
String pluginsDir = System.getProperty(PLUGINS_DIR_PROPERTY_NAME);
if (pluginsDir == null) {
pluginsDir = isDevelopment() ? DEVELOPMENT_PLUGINS_DIR : DEFAULT_PLUGINS_DIR;
if (pluginsDir != null && !pluginsDir.isEmpty()) {
return Arrays.stream(pluginsDir.split(","))
.map(String::trim)
.map(Paths::get)
.collect(Collectors.toList());
}
return Paths.get(pluginsDir);
pluginsDir = isDevelopment() ? DEVELOPMENT_PLUGINS_DIR : DEFAULT_PLUGINS_DIR;
return Collections.singletonList(Paths.get(pluginsDir));
}
/**

46
pf4j/src/main/java/org/pf4j/BasePluginRepository.java

@ -22,11 +22,11 @@ import java.io.FileFilter;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Decebal Suiu
@ -34,17 +34,21 @@ import java.util.List;
*/
public class BasePluginRepository implements PluginRepository {
protected final Path pluginsRoot;
protected final List<Path> pluginsRoots;
protected FileFilter filter;
protected Comparator<File> comparator;
public BasePluginRepository(Path pluginsRoot) {
this(pluginsRoot, null);
public BasePluginRepository(Path... pluginsRoots) {
this(Arrays.asList(pluginsRoots));
}
public BasePluginRepository(Path pluginsRoot, FileFilter filter) {
this.pluginsRoot = pluginsRoot;
public BasePluginRepository(List<Path> pluginsRoots) {
this(pluginsRoots, null);
}
public BasePluginRepository(List<Path> pluginsRoots, FileFilter filter) {
this.pluginsRoots = pluginsRoots;
this.filter = filter;
// last modified file is first
@ -67,22 +71,11 @@ public class BasePluginRepository implements PluginRepository {
@Override
public List<Path> getPluginPaths() {
File[] files = pluginsRoot.toFile().listFiles(filter);
if ((files == null) || files.length == 0) {
return Collections.emptyList();
}
if (comparator != null) {
Arrays.sort(files, comparator);
}
List<Path> paths = new ArrayList<>(files.length);
for (File file : files) {
paths.add(file.toPath());
}
return paths;
return pluginsRoots.stream()
.flatMap(path -> streamFiles(path, filter))
.sorted(comparator)
.map(File::toPath)
.collect(Collectors.toList());
}
@Override
@ -101,4 +94,11 @@ public class BasePluginRepository implements PluginRepository {
}
}
protected Stream<File> streamFiles(Path directory, FileFilter filter) {
File[] files = directory.toFile().listFiles(filter);
return files != null
? Arrays.stream(files)
: Stream.empty();
}
}

21
pf4j/src/main/java/org/pf4j/DefaultPluginManager.java

@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
/**
* Default implementation of the {@link PluginManager} interface.
@ -41,8 +42,12 @@ public class DefaultPluginManager extends AbstractPluginManager {
super();
}
public DefaultPluginManager(Path pluginsRoot) {
super(pluginsRoot);
public DefaultPluginManager(Path... pluginsRoots) {
super(pluginsRoots);
}
public DefaultPluginManager(List<Path> pluginsRoots) {
super(pluginsRoots);
}
@Override
@ -73,7 +78,11 @@ public class DefaultPluginManager extends AbstractPluginManager {
@Override
protected PluginStatusProvider createPluginStatusProvider() {
String configDir = System.getProperty(PLUGINS_DIR_CONFIG_PROPERTY_NAME);
Path configPath = configDir != null ? Paths.get(configDir) : getPluginsRoot();
Path configPath = configDir != null
? Paths.get(configDir)
: getPluginsRoots().stream()
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No pluginsRoot configured"));
return new DefaultPluginStatusProvider(configPath);
}
@ -81,9 +90,9 @@ public class DefaultPluginManager extends AbstractPluginManager {
@Override
protected PluginRepository createPluginRepository() {
return new CompoundPluginRepository()
.add(new DevelopmentPluginRepository(getPluginsRoot()), this::isDevelopment)
.add(new JarPluginRepository(getPluginsRoot()), this::isNotDevelopment)
.add(new DefaultPluginRepository(getPluginsRoot()), this::isNotDevelopment);
.add(new DevelopmentPluginRepository(getPluginsRoots()), this::isDevelopment)
.add(new JarPluginRepository(getPluginsRoots()), this::isNotDevelopment)
.add(new DefaultPluginRepository(getPluginsRoots()), this::isNotDevelopment);
}
@Override

25
pf4j/src/main/java/org/pf4j/DefaultPluginRepository.java

@ -29,6 +29,7 @@ import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
/**
@ -38,8 +39,12 @@ public class DefaultPluginRepository extends BasePluginRepository {
private static final Logger log = LoggerFactory.getLogger(DefaultPluginRepository.class);
public DefaultPluginRepository(Path pluginsRoot) {
super(pluginsRoot);
public DefaultPluginRepository(Path... pluginsRoots) {
this(Arrays.asList(pluginsRoots));
}
public DefaultPluginRepository(List<Path> pluginsRoots) {
super(pluginsRoots);
AndFileFilter pluginsFilter = new AndFileFilter(new DirectoryFileFilter());
pluginsFilter.addFileFilter(new NotFileFilter(createHiddenPluginFilter()));
@ -64,17 +69,19 @@ public class DefaultPluginRepository extends BasePluginRepository {
private void extractZipFiles() {
// expand plugins zip files
File[] zipFiles = pluginsRoot.toFile().listFiles(new ZipFileFilter());
if ((zipFiles != null) && zipFiles.length > 0) {
for (File pluginZip : zipFiles) {
pluginsRoots.stream()
.flatMap(path -> streamFiles(path, new ZipFileFilter()))
.map(File::toPath)
.forEach(this::expandIfZip);
}
private void expandIfZip(Path filePath) {
try {
FileUtils.expandIfZip(pluginZip.toPath());
FileUtils.expandIfZip(filePath);
} catch (IOException e) {
log.error("Cannot expand plugin zip '{}'", pluginZip);
log.error("Cannot expand plugin zip '{}'", filePath);
log.error(e.getMessage(), e);
}
}
}
}
}

10
pf4j/src/main/java/org/pf4j/DevelopmentPluginRepository.java

@ -24,6 +24,8 @@ import org.pf4j.util.OrFileFilter;
import java.io.FileFilter;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
/**
* @author Decebal Suiu
@ -33,8 +35,12 @@ public class DevelopmentPluginRepository extends BasePluginRepository {
public static final String MAVEN_BUILD_DIR = "target";
public static final String GRADLE_BUILD_DIR = "build";
public DevelopmentPluginRepository(Path pluginsRoot) {
super(pluginsRoot);
public DevelopmentPluginRepository(Path... pluginsRoots) {
this(Arrays.asList(pluginsRoots));
}
public DevelopmentPluginRepository(List<Path> pluginsRoots) {
super(pluginsRoots);
AndFileFilter pluginsFilter = new AndFileFilter(new DirectoryFileFilter());
pluginsFilter.addFileFilter(new NotFileFilter(createHiddenPluginFilter()));

8
pf4j/src/main/java/org/pf4j/JarPluginManager.java

@ -30,8 +30,8 @@ public class JarPluginManager extends DefaultPluginManager {
super();
}
public JarPluginManager(Path pluginsRoot) {
super(pluginsRoot);
public JarPluginManager(Path... pluginsRoots) {
super(pluginsRoots);
}
@Override
@ -49,8 +49,8 @@ public class JarPluginManager extends DefaultPluginManager {
@Override
protected PluginRepository createPluginRepository() {
return new CompoundPluginRepository()
.add(new DevelopmentPluginRepository(getPluginsRoot()), this::isDevelopment)
.add(new JarPluginRepository(getPluginsRoot()), this::isNotDevelopment);
.add(new DevelopmentPluginRepository(getPluginsRoots()), this::isDevelopment)
.add(new JarPluginRepository(getPluginsRoots()), this::isNotDevelopment);
}
}

10
pf4j/src/main/java/org/pf4j/JarPluginRepository.java

@ -18,14 +18,20 @@ package org.pf4j;
import org.pf4j.util.JarFileFilter;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
/**
* @author Decebal Suiu
*/
public class JarPluginRepository extends BasePluginRepository {
public JarPluginRepository(Path pluginsRoot) {
super(pluginsRoot, new JarFileFilter());
public JarPluginRepository(Path... pluginsRoots) {
this(Arrays.asList(pluginsRoots));
}
public JarPluginRepository(List<Path> pluginsRoots) {
super(pluginsRoots, new JarFileFilter());
}
}

12
pf4j/src/main/java/org/pf4j/PluginManager.java

@ -205,12 +205,22 @@ public interface PluginManager {
String getSystemVersion();
/**
* Gets the path of the folder where plugins are installed.
* Gets the first path of the folders where plugins are installed.
*
* @deprecated Use {@link #getPluginsRoots()} instead to get all paths where plugins are could be installed.
*
* @return Path of plugins root
*/
@Deprecated
Path getPluginsRoot();
/**
* Gets the a read-only list of all paths of the folders where plugins are installed.
*
* @return Paths of plugins roots
*/
List<Path> getPluginsRoots();
VersionManager getVersionManager();
}

4
pf4j/src/main/java/org/pf4j/ZipPluginManager.java

@ -42,8 +42,8 @@ public class ZipPluginManager extends DefaultPluginManager {
@Override
protected PluginRepository createPluginRepository() {
return new CompoundPluginRepository()
.add(new DevelopmentPluginRepository(getPluginsRoot()), this::isDevelopment)
.add(new DefaultPluginRepository(getPluginsRoot()), this::isNotDevelopment);
.add(new DevelopmentPluginRepository(getPluginsRoots()), this::isDevelopment)
.add(new DefaultPluginRepository(getPluginsRoots()), this::isNotDevelopment);
}
}

59
pf4j/src/test/java/org/pf4j/DefaultPluginRepositoryTest.java

@ -15,10 +15,11 @@
*/
package org.pf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.pf4j.plugin.PluginZip;
import org.pf4j.util.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
@ -35,18 +36,40 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
public class DefaultPluginRepositoryTest {
@TempDir
Path pluginsPath;
Path pluginsPath1;
Path pluginsPath2;
@BeforeEach
public void setUp() throws IOException {
Path plugin1Path = Files.createDirectory(pluginsPath.resolve("plugin-1"));
pluginsPath1 = Files.createTempDirectory("junit-pf4j-");
pluginsPath2 = Files.createTempDirectory("junit-pf4j-");
Path plugin1Path = Files.createDirectory(pluginsPath1.resolve("plugin-1"));
// Prove that we can delete a folder with a file inside
Files.createFile(plugin1Path.resolve("myfile"));
// Create a zip file for plugin-1 to test that it is deleted when plugin is deleted
new PluginZip.Builder(pluginsPath.resolve("plugin-1.zip"), "plugin-1").pluginVersion("1.0").build();
Files.createDirectory(pluginsPath.resolve("plugin-2"));
Files.createDirectory(pluginsPath.resolve("plugin-3"));
new PluginZip.Builder(pluginsPath1.resolve("plugin-1.zip"), "plugin-1").pluginVersion("1.0").build();
Files.createDirectory(pluginsPath2.resolve("plugin-2"));
Files.createDirectory(pluginsPath2.resolve("plugin-3"));
}
@AfterEach
public void tearDown() throws IOException {
FileUtils.delete(pluginsPath1);
FileUtils.delete(pluginsPath2);
}
/**
* Test of {@link DefaultPluginRepository#getPluginPaths()} method.
*/
@Test
public void testGetPluginArchivesFromSinglePath() {
PluginRepository repository = new DefaultPluginRepository(pluginsPath2);
List<Path> pluginPaths = repository.getPluginPaths();
assertEquals(2, pluginPaths.size());
assertPathExists(pluginPaths, pluginsPath2.resolve("plugin-2"));
assertPathExists(pluginPaths, pluginsPath2.resolve("plugin-3"));
}
/**
@ -54,14 +77,14 @@ public class DefaultPluginRepositoryTest {
*/
@Test
public void testGetPluginArchives() {
PluginRepository repository = new DefaultPluginRepository(pluginsPath);
PluginRepository repository = new DefaultPluginRepository(pluginsPath1, pluginsPath2);
List<Path> pluginPaths = repository.getPluginPaths();
assertEquals(3, pluginPaths.size());
assertPathExists(pluginPaths, pluginsPath.resolve("plugin-1"));
assertPathExists(pluginPaths, pluginsPath.resolve("plugin-2"));
assertPathExists(pluginPaths, pluginsPath.resolve("plugin-3"));
assertPathExists(pluginPaths, pluginsPath1.resolve("plugin-1"));
assertPathExists(pluginPaths, pluginsPath2.resolve("plugin-2"));
assertPathExists(pluginPaths, pluginsPath2.resolve("plugin-3"));
}
/**
@ -69,18 +92,18 @@ public class DefaultPluginRepositoryTest {
*/
@Test
public void testDeletePluginPath() {
PluginRepository repository = new DefaultPluginRepository(pluginsPath);
PluginRepository repository = new DefaultPluginRepository(pluginsPath1, pluginsPath2);
assertTrue(Files.exists(pluginsPath.resolve("plugin-1.zip")));
assertTrue(repository.deletePluginPath(pluginsPath.resolve("plugin-1")));
assertFalse(Files.exists(pluginsPath.resolve("plugin-1.zip")));
assertTrue(repository.deletePluginPath(pluginsPath.resolve("plugin-3")));
assertFalse(repository.deletePluginPath(pluginsPath.resolve("plugin-4")));
assertTrue(Files.exists(pluginsPath1.resolve("plugin-1.zip")));
assertTrue(repository.deletePluginPath(pluginsPath1.resolve("plugin-1")));
assertFalse(Files.exists(pluginsPath1.resolve("plugin-1.zip")));
assertTrue(repository.deletePluginPath(pluginsPath2.resolve("plugin-3")));
assertFalse(repository.deletePluginPath(pluginsPath2.resolve("plugin-4")));
List<Path> pluginPaths = repository.getPluginPaths();
assertEquals(1, pluginPaths.size());
assertEquals(pluginsPath.relativize(pluginPaths.get(0)).toString(), "plugin-2");
assertEquals(pluginsPath2.relativize(pluginPaths.get(0)).toString(), "plugin-2");
}
private void assertPathExists(List<Path> paths, Path path) {

75
pf4j/src/test/java/org/pf4j/LoadPluginsFromMultipleRootsTest.java

@ -0,0 +1,75 @@
/*
* 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;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.pf4j.plugin.PluginZip;
import org.pf4j.util.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class LoadPluginsFromMultipleRootsTest {
private DefaultPluginManager pluginManager;
Path pluginsPath1;
Path pluginsPath2;
@BeforeEach
public void setUp() throws IOException {
pluginsPath1 = Files.createTempDirectory("junit-pf4j-");
pluginsPath2 = Files.createTempDirectory("junit-pf4j-");
pluginManager = new DefaultPluginManager(pluginsPath1, pluginsPath2);
}
@AfterEach
public void tearDown() throws IOException {
FileUtils.delete(pluginsPath1);
FileUtils.delete(pluginsPath2);
}
@Test
public void load() throws Exception {
PluginZip pluginZip1 = new PluginZip.Builder(pluginsPath1.resolve("my-plugin-1.2.3.zip"), "myPlugin")
.pluginVersion("1.2.3")
.build();
PluginZip pluginZip2 = new PluginZip.Builder(pluginsPath2.resolve("my-other-plugin-4.5.6.zip"), "myOtherPlugin")
.pluginVersion("4.5.6")
.build();
assertTrue(Files.exists(pluginZip1.path()));
assertEquals(0, pluginManager.getPlugins().size());
pluginManager.loadPlugins();
assertTrue(Files.exists(pluginZip1.path()));
assertTrue(Files.exists(pluginZip1.unzippedPath()));
assertTrue(Files.exists(pluginZip2.path()));
assertTrue(Files.exists(pluginZip2.unzippedPath()));
assertEquals(2, pluginManager.getPlugins().size());
assertEquals(pluginZip1.pluginId(), pluginManager.idForPath(pluginZip1.unzippedPath()));
assertEquals(pluginZip2.pluginId(), pluginManager.idForPath(pluginZip2.unzippedPath()));
}
}

6
pf4j/src/test/java/org/pf4j/LoadPluginsTest.java

@ -23,6 +23,7 @@ import org.pf4j.plugin.PluginZip;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
@ -200,6 +201,11 @@ public class LoadPluginsTest {
assertEquals(pluginsPath, pluginManager.getPluginsRoot());
}
@Test
public void getRoots() {
assertEquals(Collections.singletonList(pluginsPath), pluginManager.getPluginsRoots());
}
@Test
public void notAPlugin() {
pluginsPath.resolve("not-a-zip");

Loading…
Cancel
Save