You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AbstractPluginManager.java 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. /*
  2. * Copyright 2016 Decebal Suiu
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.pf4j;
  17. import org.slf4j.Logger;
  18. import org.slf4j.LoggerFactory;
  19. import org.pf4j.util.StringUtils;
  20. import java.io.Closeable;
  21. import java.io.IOException;
  22. import java.nio.file.Files;
  23. import java.nio.file.Path;
  24. import java.nio.file.Paths;
  25. import java.util.ArrayList;
  26. import java.util.Collections;
  27. import java.util.HashMap;
  28. import java.util.Iterator;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.Set;
  32. /**
  33. * This class implements the boilerplate plugin code that any {@link PluginManager}
  34. * implementation would have to support.
  35. * It helps cut the noise out of the subclass that handles plugin management.
  36. *
  37. * @author Decebal Suiu
  38. */
  39. public abstract class AbstractPluginManager implements PluginManager {
  40. private static final Logger log = LoggerFactory.getLogger(AbstractPluginManager.class);
  41. private Path pluginsRoot;
  42. private ExtensionFinder extensionFinder;
  43. private PluginDescriptorFinder pluginDescriptorFinder;
  44. /*
  45. * A map of plugins this manager is responsible for (the key is the 'pluginId').
  46. */
  47. protected Map<String, PluginWrapper> plugins;
  48. /*
  49. * A map of plugin class loaders (the key is the 'pluginId').
  50. */
  51. private Map<String, ClassLoader> pluginClassLoaders;
  52. /*
  53. * A list with unresolved plugins (unresolved dependency).
  54. */
  55. private List<PluginWrapper> unresolvedPlugins;
  56. /**
  57. * A list with all resolved plugins (resolved dependency).
  58. */
  59. private List<PluginWrapper> resolvedPlugins;
  60. /*
  61. * A list with started plugins.
  62. */
  63. private List<PluginWrapper> startedPlugins;
  64. /*
  65. * The registered {@link PluginStateListener}s.
  66. */
  67. private List<PluginStateListener> pluginStateListeners;
  68. /*
  69. * Cache value for the runtime mode.
  70. * No need to re-read it because it wont change at runtime.
  71. */
  72. private RuntimeMode runtimeMode;
  73. /*
  74. * The system version used for comparisons to the plugin requires attribute.
  75. */
  76. private String systemVersion = "0.0.0";
  77. private PluginRepository pluginRepository;
  78. private PluginFactory pluginFactory;
  79. private ExtensionFactory extensionFactory;
  80. private PluginStatusProvider pluginStatusProvider;
  81. private DependencyResolver dependencyResolver;
  82. private PluginLoader pluginLoader;
  83. private boolean exactVersionAllowed = false;
  84. private VersionManager versionManager;
  85. /**
  86. * The plugins root is supplied by {@code System.getProperty("pf4j.pluginsDir", "plugins")}.
  87. */
  88. public AbstractPluginManager() {
  89. initialize();
  90. }
  91. /**
  92. * Constructs {@code AbstractPluginManager} with the given plugins root.
  93. *
  94. * @param pluginsRoot the root to search for plugins
  95. */
  96. public AbstractPluginManager(Path pluginsRoot) {
  97. this.pluginsRoot = pluginsRoot;
  98. initialize();
  99. }
  100. @Override
  101. public void setSystemVersion(String version) {
  102. systemVersion = version;
  103. }
  104. @Override
  105. public String getSystemVersion() {
  106. return systemVersion;
  107. }
  108. /**
  109. * Returns a copy of plugins.
  110. *
  111. * @return
  112. */
  113. @Override
  114. public List<PluginWrapper> getPlugins() {
  115. return new ArrayList<>(plugins.values());
  116. }
  117. /**
  118. * Returns a copy of plugins with that state.
  119. *
  120. * @param pluginState
  121. * @return
  122. */
  123. @Override
  124. public List<PluginWrapper> getPlugins(PluginState pluginState) {
  125. List<PluginWrapper> plugins = new ArrayList<>();
  126. for (PluginWrapper plugin : getPlugins()) {
  127. if (pluginState.equals(plugin.getPluginState())) {
  128. plugins.add(plugin);
  129. }
  130. }
  131. return plugins;
  132. }
  133. @Override
  134. public List<PluginWrapper> getResolvedPlugins() {
  135. return resolvedPlugins;
  136. }
  137. @Override
  138. public List<PluginWrapper> getUnresolvedPlugins() {
  139. return unresolvedPlugins;
  140. }
  141. @Override
  142. public List<PluginWrapper> getStartedPlugins() {
  143. return startedPlugins;
  144. }
  145. @Override
  146. public PluginWrapper getPlugin(String pluginId) {
  147. return plugins.get(pluginId);
  148. }
  149. @Override
  150. public String loadPlugin(Path pluginPath) {
  151. if ((pluginPath == null) || Files.notExists(pluginPath)) {
  152. throw new IllegalArgumentException(String.format("Specified plugin %s does not exist!", pluginPath));
  153. }
  154. log.debug("Loading plugin from '{}'", pluginPath);
  155. try {
  156. PluginWrapper pluginWrapper = loadPluginFromPath(pluginPath);
  157. // TODO uninstalled plugin dependencies?
  158. getUnresolvedPlugins().remove(pluginWrapper);
  159. getResolvedPlugins().add(pluginWrapper);
  160. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, null));
  161. return pluginWrapper.getDescriptor().getPluginId();
  162. } catch (PluginException e) {
  163. log.error(e.getMessage(), e);
  164. }
  165. return null;
  166. }
  167. /**
  168. * Load plugins.
  169. */
  170. @Override
  171. public void loadPlugins() {
  172. log.debug("Lookup plugins in '{}'", pluginsRoot);
  173. // check for plugins root
  174. if (Files.notExists(pluginsRoot) || !Files.isDirectory(pluginsRoot)) {
  175. log.error("No '{}' root", pluginsRoot);
  176. return;
  177. }
  178. // get all plugin paths from repository
  179. List<Path> pluginPaths = pluginRepository.getPluginPaths();
  180. // check for no plugins
  181. if (pluginPaths.isEmpty()) {
  182. log.info("No plugins");
  183. return;
  184. }
  185. log.debug("Found {} possible plugins: {}", pluginPaths.size(), pluginPaths);
  186. // load plugins from plugin paths
  187. // TODO
  188. for (Path pluginPath : pluginPaths) {
  189. try {
  190. loadPluginFromPath(pluginPath);
  191. } catch (PluginException e) {
  192. log.error(e.getMessage(), e);
  193. }
  194. }
  195. // resolve 'unresolvedPlugins'
  196. try {
  197. resolvePlugins();
  198. } catch (PluginException e) {
  199. log.error(e.getMessage(), e);
  200. }
  201. }
  202. /**
  203. * Unload the specified plugin and it's dependents.
  204. */
  205. @Override
  206. public boolean unloadPlugin(String pluginId) {
  207. return unloadPlugin(pluginId, true);
  208. }
  209. private boolean unloadPlugin(String pluginId, boolean unloadDependents) {
  210. try {
  211. if (unloadDependents) {
  212. List<String> dependents = dependencyResolver.getDependents(pluginId);
  213. while (!dependents.isEmpty()) {
  214. String dependent = dependents.remove(0);
  215. unloadPlugin(dependent, false);
  216. dependents.addAll(0, dependencyResolver.getDependents(dependent));
  217. }
  218. }
  219. PluginState pluginState = stopPlugin(pluginId, false);
  220. if (PluginState.STARTED == pluginState) {
  221. return false;
  222. }
  223. PluginWrapper pluginWrapper = getPlugin(pluginId);
  224. log.info("Unload plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
  225. // remove the plugin
  226. plugins.remove(pluginId);
  227. getResolvedPlugins().remove(pluginWrapper);
  228. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  229. // remove the classloader
  230. Map<String, ClassLoader> pluginClassLoaders = getPluginClassLoaders();
  231. if (pluginClassLoaders.containsKey(pluginId)) {
  232. ClassLoader classLoader = pluginClassLoaders.remove(pluginId);
  233. if (classLoader instanceof Closeable) {
  234. try {
  235. ((Closeable) classLoader).close();
  236. } catch (IOException e) {
  237. log.error("Cannot close classloader", e);
  238. }
  239. }
  240. }
  241. return true;
  242. } catch (IllegalArgumentException e) {
  243. // ignore not found exceptions because this method is recursive
  244. }
  245. return false;
  246. }
  247. @Override
  248. public boolean deletePlugin(String pluginId) {
  249. checkPluginId(pluginId);
  250. PluginWrapper pluginWrapper = getPlugin(pluginId);
  251. PluginState pluginState = stopPlugin(pluginId);
  252. if (PluginState.STARTED == pluginState) {
  253. log.error("Failed to stop plugin '{}' on delete", pluginId);
  254. return false;
  255. }
  256. if (!unloadPlugin(pluginId)) {
  257. log.error("Failed to unload plugin '{}' on delete", pluginId);
  258. return false;
  259. }
  260. Path pluginPath = pluginWrapper.getPluginPath();
  261. return pluginRepository.deletePluginPath(pluginPath);
  262. }
  263. /**
  264. * Start all active plugins.
  265. */
  266. @Override
  267. public void startPlugins() {
  268. for (PluginWrapper pluginWrapper : resolvedPlugins) {
  269. PluginState pluginState = pluginWrapper.getPluginState();
  270. if ((PluginState.DISABLED != pluginState) && (PluginState.STARTED != pluginState)) {
  271. try {
  272. log.info("Start plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
  273. pluginWrapper.getPlugin().start();
  274. pluginWrapper.setPluginState(PluginState.STARTED);
  275. startedPlugins.add(pluginWrapper);
  276. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  277. } catch (PluginException e) {
  278. log.error(e.getMessage(), e);
  279. }
  280. }
  281. }
  282. }
  283. /**
  284. * Start the specified plugin and it's dependencies.
  285. */
  286. @Override
  287. public PluginState startPlugin(String pluginId) {
  288. checkPluginId(pluginId);
  289. PluginWrapper pluginWrapper = getPlugin(pluginId);
  290. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  291. PluginState pluginState = pluginWrapper.getPluginState();
  292. if (PluginState.STARTED == pluginState) {
  293. log.debug("Already started plugin '{}'", getPluginLabel(pluginDescriptor));
  294. return PluginState.STARTED;
  295. }
  296. if (PluginState.DISABLED == pluginState) {
  297. // automatically enable plugin on manual plugin start
  298. if (!enablePlugin(pluginId)) {
  299. return pluginState;
  300. }
  301. }
  302. for (PluginDependency dependency : pluginDescriptor.getDependencies()) {
  303. startPlugin(dependency.getPluginId());
  304. }
  305. try {
  306. log.info("Start plugin '{}'", getPluginLabel(pluginDescriptor));
  307. pluginWrapper.getPlugin().start();
  308. pluginWrapper.setPluginState(PluginState.STARTED);
  309. startedPlugins.add(pluginWrapper);
  310. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  311. } catch (PluginException e) {
  312. log.error(e.getMessage(), e);
  313. }
  314. return pluginWrapper.getPluginState();
  315. }
  316. /**
  317. * Stop all active plugins.
  318. */
  319. @Override
  320. public void stopPlugins() {
  321. // stop started plugins in reverse order
  322. Collections.reverse(startedPlugins);
  323. Iterator<PluginWrapper> itr = startedPlugins.iterator();
  324. while (itr.hasNext()) {
  325. PluginWrapper pluginWrapper = itr.next();
  326. PluginState pluginState = pluginWrapper.getPluginState();
  327. if (PluginState.STARTED == pluginState) {
  328. try {
  329. log.info("Stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
  330. pluginWrapper.getPlugin().stop();
  331. pluginWrapper.setPluginState(PluginState.STOPPED);
  332. itr.remove();
  333. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  334. } catch (PluginException e) {
  335. log.error(e.getMessage(), e);
  336. }
  337. }
  338. }
  339. }
  340. /**
  341. * Stop the specified plugin and it's dependents.
  342. */
  343. @Override
  344. public PluginState stopPlugin(String pluginId) {
  345. return stopPlugin(pluginId, true);
  346. }
  347. private PluginState stopPlugin(String pluginId, boolean stopDependents) {
  348. checkPluginId(pluginId);
  349. PluginWrapper pluginWrapper = getPlugin(pluginId);
  350. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  351. PluginState pluginState = pluginWrapper.getPluginState();
  352. if (PluginState.STOPPED == pluginState) {
  353. log.debug("Already stopped plugin '{}'", getPluginLabel(pluginDescriptor));
  354. return PluginState.STOPPED;
  355. }
  356. // test for disabled plugin
  357. if (PluginState.DISABLED == pluginState) {
  358. // do nothing
  359. return pluginState;
  360. }
  361. if (stopDependents) {
  362. List<String> dependents = dependencyResolver.getDependents(pluginId);
  363. while (!dependents.isEmpty()) {
  364. String dependent = dependents.remove(0);
  365. stopPlugin(dependent, false);
  366. dependents.addAll(0, dependencyResolver.getDependents(dependent));
  367. }
  368. }
  369. try {
  370. log.info("Stop plugin '{}'", getPluginLabel(pluginDescriptor));
  371. pluginWrapper.getPlugin().stop();
  372. pluginWrapper.setPluginState(PluginState.STOPPED);
  373. startedPlugins.remove(pluginWrapper);
  374. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  375. } catch (PluginException e) {
  376. log.error(e.getMessage(), e);
  377. }
  378. return pluginWrapper.getPluginState();
  379. }
  380. private void checkPluginId(String pluginId) {
  381. if (!plugins.containsKey(pluginId)) {
  382. throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
  383. }
  384. }
  385. @Override
  386. public boolean disablePlugin(String pluginId) {
  387. checkPluginId(pluginId);
  388. PluginWrapper pluginWrapper = getPlugin(pluginId);
  389. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  390. PluginState pluginState = pluginWrapper.getPluginState();
  391. if (PluginState.DISABLED == pluginState) {
  392. log.debug("Already disabled plugin '{}'", getPluginLabel(pluginDescriptor));
  393. return true;
  394. }
  395. if (PluginState.STOPPED == stopPlugin(pluginId)) {
  396. pluginWrapper.setPluginState(PluginState.DISABLED);
  397. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, PluginState.STOPPED));
  398. if (!pluginStatusProvider.disablePlugin(pluginId)) {
  399. return false;
  400. }
  401. log.info("Disabled plugin '{}'", getPluginLabel(pluginDescriptor));
  402. return true;
  403. }
  404. return false;
  405. }
  406. @Override
  407. public boolean enablePlugin(String pluginId) {
  408. checkPluginId(pluginId);
  409. PluginWrapper pluginWrapper = getPlugin(pluginId);
  410. if (!isPluginValid(pluginWrapper)) {
  411. log.warn("Plugin '{}' can not be enabled", getPluginLabel(pluginWrapper.getDescriptor()));
  412. return false;
  413. }
  414. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  415. PluginState pluginState = pluginWrapper.getPluginState();
  416. if (PluginState.DISABLED != pluginState) {
  417. log.debug("Plugin '{}' is not disabled", getPluginLabel(pluginDescriptor));
  418. return true;
  419. }
  420. if (!pluginStatusProvider.enablePlugin(pluginId)) {
  421. return false;
  422. }
  423. pluginWrapper.setPluginState(PluginState.CREATED);
  424. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  425. log.info("Enabled plugin '{}'", getPluginLabel(pluginDescriptor));
  426. return true;
  427. }
  428. /**
  429. * Get the {@link ClassLoader} for plugin.
  430. */
  431. @Override
  432. public ClassLoader getPluginClassLoader(String pluginId) {
  433. return pluginClassLoaders.get(pluginId);
  434. }
  435. @Override
  436. public <T> List<T> getExtensions(Class<T> type) {
  437. List<ExtensionWrapper<T>> extensionsWrapper = extensionFinder.find(type);
  438. List<T> extensions = new ArrayList<>(extensionsWrapper.size());
  439. for (ExtensionWrapper<T> extensionWrapper : extensionsWrapper) {
  440. extensions.add(extensionWrapper.getExtension());
  441. }
  442. return extensions;
  443. }
  444. @Override
  445. public <T> List<T> getExtensions(Class<T> type, String pluginId) {
  446. List<ExtensionWrapper<T>> extensionsWrapper = extensionFinder.find(type, pluginId);
  447. List<T> extensions = new ArrayList<>(extensionsWrapper.size());
  448. for (ExtensionWrapper<T> extensionWrapper : extensionsWrapper) {
  449. extensions.add(extensionWrapper.getExtension());
  450. }
  451. return extensions;
  452. }
  453. @Override
  454. @SuppressWarnings("unchecked")
  455. public List getExtensions(String pluginId) {
  456. List<ExtensionWrapper> extensionsWrapper = extensionFinder.find(pluginId);
  457. List extensions = new ArrayList<>(extensionsWrapper.size());
  458. for (ExtensionWrapper extensionWrapper : extensionsWrapper) {
  459. extensions.add(extensionWrapper.getExtension());
  460. }
  461. return extensions;
  462. }
  463. @Override
  464. public Set<String> getExtensionClassNames(String pluginId) {
  465. return extensionFinder.findClassNames(pluginId);
  466. }
  467. @Override
  468. public ExtensionFactory getExtensionFactory() {
  469. return extensionFactory;
  470. }
  471. // TODO remove
  472. public PluginLoader getPluginLoader() {
  473. return pluginLoader;
  474. }
  475. public Path getPluginsRoot() {
  476. return pluginsRoot;
  477. }
  478. @Override
  479. public RuntimeMode getRuntimeMode() {
  480. if (runtimeMode == null) {
  481. // retrieves the runtime mode from system
  482. String modeAsString = System.getProperty("pf4j.mode", RuntimeMode.DEPLOYMENT.toString());
  483. runtimeMode = RuntimeMode.byName(modeAsString);
  484. }
  485. return runtimeMode;
  486. }
  487. @Override
  488. public PluginWrapper whichPlugin(Class<?> clazz) {
  489. ClassLoader classLoader = clazz.getClassLoader();
  490. for (PluginWrapper plugin : resolvedPlugins) {
  491. if (plugin.getPluginClassLoader() == classLoader) {
  492. return plugin;
  493. }
  494. }
  495. return null;
  496. }
  497. @Override
  498. public synchronized void addPluginStateListener(PluginStateListener listener) {
  499. pluginStateListeners.add(listener);
  500. }
  501. @Override
  502. public synchronized void removePluginStateListener(PluginStateListener listener) {
  503. pluginStateListeners.remove(listener);
  504. }
  505. public String getVersion() {
  506. String version = null;
  507. Package pf4jPackage = PluginManager.class.getPackage();
  508. if (pf4jPackage != null) {
  509. version = pf4jPackage.getImplementationVersion();
  510. if (version == null) {
  511. version = pf4jPackage.getSpecificationVersion();
  512. }
  513. }
  514. return (version != null) ? version : "0.0.0";
  515. }
  516. protected abstract PluginRepository createPluginRepository();
  517. protected abstract PluginFactory createPluginFactory();
  518. protected abstract ExtensionFactory createExtensionFactory();
  519. protected abstract PluginDescriptorFinder createPluginDescriptorFinder();
  520. protected abstract ExtensionFinder createExtensionFinder();
  521. protected abstract PluginStatusProvider createPluginStatusProvider();
  522. protected abstract PluginLoader createPluginLoader();
  523. protected abstract VersionManager createVersionManager();
  524. protected PluginDescriptorFinder getPluginDescriptorFinder() {
  525. return pluginDescriptorFinder;
  526. }
  527. protected PluginFactory getPluginFactory() {
  528. return pluginFactory;
  529. }
  530. protected Map<String, ClassLoader> getPluginClassLoaders() {
  531. return pluginClassLoaders;
  532. }
  533. protected void initialize() {
  534. plugins = new HashMap<>();
  535. pluginClassLoaders = new HashMap<>();
  536. unresolvedPlugins = new ArrayList<>();
  537. resolvedPlugins = new ArrayList<>();
  538. startedPlugins = new ArrayList<>();
  539. pluginStateListeners = new ArrayList<>();
  540. if (pluginsRoot == null) {
  541. pluginsRoot = createPluginsRoot();
  542. }
  543. System.setProperty("pf4j.pluginsDir", pluginsRoot.toString());
  544. pluginRepository = createPluginRepository();
  545. pluginFactory = createPluginFactory();
  546. extensionFactory = createExtensionFactory();
  547. pluginDescriptorFinder = createPluginDescriptorFinder();
  548. extensionFinder = createExtensionFinder();
  549. pluginStatusProvider = createPluginStatusProvider();
  550. pluginLoader = createPluginLoader();
  551. versionManager = createVersionManager();
  552. dependencyResolver = new DependencyResolver(versionManager);
  553. }
  554. /**
  555. * Add the possibility to override the plugins root.
  556. * If a {@code pf4j.pluginsDir} system property is defined than this method returns that root.
  557. * If {@link #getRuntimeMode()} returns {@link RuntimeMode#DEVELOPMENT} than {@code ../plugins}
  558. * is returned else this method returns {@code plugins}.
  559. *
  560. * @return the plugins root
  561. */
  562. protected Path createPluginsRoot() {
  563. String pluginsDir = System.getProperty("pf4j.pluginsDir");
  564. if (pluginsDir == null) {
  565. if (isDevelopment()) {
  566. pluginsDir = "../plugins";
  567. } else {
  568. pluginsDir = "plugins";
  569. }
  570. }
  571. return Paths.get(pluginsDir);
  572. }
  573. /**
  574. * Check if this plugin is valid (satisfies "requires" param) for a given system version.
  575. *
  576. * @param pluginWrapper the plugin to check
  577. * @return true if plugin satisfies the "requires" or if requires was left blank
  578. */
  579. protected boolean isPluginValid(PluginWrapper pluginWrapper) {
  580. String requires = pluginWrapper.getDescriptor().getRequires().trim();
  581. if (!isExactVersionAllowed() && requires.matches("^\\d+\\.\\d+\\.\\d+$")) {
  582. // If exact versions are not allowed in requires, rewrite to >= expression
  583. requires = ">=" + requires;
  584. }
  585. if (systemVersion.equals("0.0.0") || versionManager.satisfies(requires, systemVersion)) {
  586. return true;
  587. }
  588. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  589. log.warn("Plugin '{}' requires a minimum system version of {}, and you have {}",
  590. getPluginLabel(pluginDescriptor),
  591. pluginWrapper.getDescriptor().getRequires(),
  592. getSystemVersion());
  593. return false;
  594. }
  595. protected boolean isPluginDisabled(String pluginId) {
  596. return pluginStatusProvider.isPluginDisabled(pluginId);
  597. }
  598. protected void resolvePlugins() throws PluginException {
  599. // retrieves the plugins descriptors from "unresolvedPlugins" list
  600. List<PluginDescriptor> descriptors = new ArrayList<>();
  601. for (PluginWrapper plugin : unresolvedPlugins) {
  602. descriptors.add(plugin.getDescriptor());
  603. }
  604. DependencyResolver.Result result = dependencyResolver.resolve(descriptors);
  605. if (result.hasCyclicDependency()) {
  606. throw new DependencyResolver.CyclicDependencyException();
  607. }
  608. List<String> notFoundDependencies = result.getNotFoundDependencies();
  609. if (!notFoundDependencies.isEmpty()) {
  610. throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies);
  611. }
  612. List<DependencyResolver.WrongDependencyVersion> wrongVersionDependencies = result.getWrongVersionDependencies();
  613. if (!wrongVersionDependencies.isEmpty()) {
  614. throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies);
  615. }
  616. List<String> sortedPlugins = result.getSortedPlugins();
  617. // move plugins from "unresolved" to "resolved"
  618. for (String pluginId : sortedPlugins) {
  619. PluginWrapper pluginWrapper = plugins.get(pluginId);
  620. unresolvedPlugins.remove(pluginWrapper);
  621. resolvedPlugins.add(pluginWrapper);
  622. log.info("Plugin '{}' resolved", getPluginLabel(pluginWrapper.getDescriptor()));
  623. }
  624. }
  625. protected synchronized void firePluginStateEvent(PluginStateEvent event) {
  626. for (PluginStateListener listener : pluginStateListeners) {
  627. log.debug("Fire '{}' to '{}'", event, listener);
  628. listener.pluginStateChanged(event);
  629. }
  630. }
  631. protected PluginWrapper loadPluginFromPath(Path pluginPath) throws PluginException {
  632. // test for plugin duplication
  633. String pluginId = idForPath(pluginPath);
  634. if (pluginId != null) {
  635. log.warn("Plugin '{}' already loaded with id '{}'", pluginPath, pluginId);
  636. return null;
  637. }
  638. // retrieves the plugin descriptor
  639. log.debug("Finding plugin descriptor for plugin '{}'", pluginPath);
  640. PluginDescriptor pluginDescriptor = getPluginDescriptorFinder().find(pluginPath);
  641. validatePluginDescriptor(pluginDescriptor);
  642. log.debug("Found descriptor {}", pluginDescriptor);
  643. String pluginClassName = pluginDescriptor.getPluginClass();
  644. log.debug("Class '{}' for plugin '{}'", pluginClassName, pluginPath);
  645. // load plugin
  646. log.debug("Loading plugin '{}'", pluginPath);
  647. ClassLoader pluginClassLoader = getPluginLoader().loadPlugin(pluginPath, pluginDescriptor);
  648. log.debug("Loaded plugin '{}' with class loader '{}'", pluginPath, pluginClassLoader);
  649. // create the plugin wrapper
  650. log.debug("Creating wrapper for plugin '{}'", pluginPath);
  651. PluginWrapper pluginWrapper = new PluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader);
  652. pluginWrapper.setPluginFactory(getPluginFactory());
  653. pluginWrapper.setRuntimeMode(getRuntimeMode());
  654. // test for disabled plugin
  655. if (isPluginDisabled(pluginDescriptor.getPluginId())) {
  656. log.info("Plugin '{}' is disabled", pluginPath);
  657. pluginWrapper.setPluginState(PluginState.DISABLED);
  658. }
  659. // validate the plugin
  660. if (!isPluginValid(pluginWrapper)) {
  661. log.info("Plugin '{}' is disabled", pluginPath);
  662. pluginWrapper.setPluginState(PluginState.DISABLED);
  663. }
  664. log.debug("Created wrapper '{}' for plugin '{}'", pluginWrapper, pluginPath);
  665. pluginId = pluginDescriptor.getPluginId();
  666. // add plugin to the list with plugins
  667. plugins.put(pluginId, pluginWrapper);
  668. getUnresolvedPlugins().add(pluginWrapper);
  669. // add plugin class loader to the list with class loaders
  670. getPluginClassLoaders().put(pluginId, pluginClassLoader);
  671. return pluginWrapper;
  672. }
  673. /**
  674. * Tests for already loaded plugins on given path.
  675. *
  676. * @param pluginPath the path to investigate
  677. * @return id of plugin or null if not loaded
  678. */
  679. protected String idForPath(Path pluginPath) {
  680. for (PluginWrapper plugin : plugins.values()) {
  681. if (plugin.getPluginPath().equals(pluginPath)) {
  682. return plugin.getPluginId();
  683. }
  684. }
  685. return null;
  686. }
  687. /**
  688. * Override this to change the validation criteria.
  689. *
  690. * @param descriptor the plugin descriptor to validate
  691. * @throws PluginException if validation fails
  692. */
  693. protected void validatePluginDescriptor(PluginDescriptor descriptor) throws PluginException {
  694. if (StringUtils.isEmpty(descriptor.getPluginId())) {
  695. throw new PluginException("Field 'id' cannot be empty");
  696. }
  697. if (StringUtils.isEmpty(descriptor.getPluginClass())) {
  698. throw new PluginException("Field 'class' cannot be empty");
  699. }
  700. if (descriptor.getVersion() == null) {
  701. throw new PluginException("Field 'version' cannot be empty");
  702. }
  703. }
  704. // TODO add this method in PluginManager as default method for Java 8.
  705. protected boolean isDevelopment() {
  706. return RuntimeMode.DEVELOPMENT.equals(getRuntimeMode());
  707. }
  708. /**
  709. * @return true if exact versions in requires is allowed
  710. */
  711. public boolean isExactVersionAllowed() {
  712. return exactVersionAllowed;
  713. }
  714. /**
  715. * Set to true to allow requires expression to be exactly x.y.z.
  716. * The default is false, meaning that using an exact version x.y.z will
  717. * implicitly mean the same as >=x.y.z
  718. *
  719. * @param exactVersionAllowed set to true or false
  720. */
  721. public void setExactVersionAllowed(boolean exactVersionAllowed) {
  722. this.exactVersionAllowed = exactVersionAllowed;
  723. }
  724. @Override
  725. public VersionManager getVersionManager() {
  726. return versionManager;
  727. }
  728. /**
  729. * The plugin label is used in logging and it's a string in format {@code pluginId@pluginVersion}.
  730. * @param pluginDescriptor
  731. * @return
  732. */
  733. protected String getPluginLabel(PluginDescriptor pluginDescriptor) {
  734. return pluginDescriptor.getPluginId() + "@" + pluginDescriptor.getVersion();
  735. }
  736. }