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 37KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066
  1. /*
  2. * Copyright (C) 2012-present the original author or authors.
  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.pf4j.util.StringUtils;
  18. import org.slf4j.Logger;
  19. import org.slf4j.LoggerFactory;
  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.Arrays;
  27. import java.util.Collections;
  28. import java.util.HashMap;
  29. import java.util.Iterator;
  30. import java.util.List;
  31. import java.util.Map;
  32. import java.util.Set;
  33. import java.util.stream.Collectors;
  34. /**
  35. * This class implements the boilerplate plugin code that any {@link PluginManager}
  36. * implementation would have to support.
  37. * It helps cut the noise out of the subclass that handles plugin management.
  38. * <p>
  39. * This class is not thread-safe.
  40. *
  41. * @author Decebal Suiu
  42. */
  43. public abstract class AbstractPluginManager implements PluginManager {
  44. private static final Logger log = LoggerFactory.getLogger(AbstractPluginManager.class);
  45. public static final String PLUGINS_DIR_PROPERTY_NAME = "pf4j.pluginsDir";
  46. public static final String MODE_PROPERTY_NAME = "pf4j.mode";
  47. public static final String DEFAULT_PLUGINS_DIR = "plugins";
  48. public static final String DEVELOPMENT_PLUGINS_DIR = "../plugins";
  49. protected final List<Path> pluginsRoots = new ArrayList<>();
  50. protected ExtensionFinder extensionFinder;
  51. protected PluginDescriptorFinder pluginDescriptorFinder;
  52. /**
  53. * A map of plugins this manager is responsible for (the key is the 'pluginId').
  54. */
  55. protected Map<String, PluginWrapper> plugins;
  56. /**
  57. * A map of plugin class loaders (the key is the 'pluginId').
  58. */
  59. protected Map<String, ClassLoader> pluginClassLoaders;
  60. /**
  61. * A list with unresolved plugins (unresolved dependency).
  62. */
  63. protected List<PluginWrapper> unresolvedPlugins;
  64. /**
  65. * A list with all resolved plugins (resolved dependency).
  66. */
  67. protected List<PluginWrapper> resolvedPlugins;
  68. /**
  69. * A list with started plugins.
  70. */
  71. protected List<PluginWrapper> startedPlugins;
  72. /**
  73. * The registered {@link PluginStateListener}s.
  74. */
  75. protected List<PluginStateListener> pluginStateListeners;
  76. /**
  77. * Cache value for the runtime mode.
  78. * No need to re-read it because it won't change at runtime.
  79. */
  80. protected RuntimeMode runtimeMode;
  81. /**
  82. * The system version used for comparisons to the plugin requires attribute.
  83. */
  84. protected String systemVersion = "0.0.0";
  85. protected PluginRepository pluginRepository;
  86. protected PluginFactory pluginFactory;
  87. protected ExtensionFactory extensionFactory;
  88. protected PluginStatusProvider pluginStatusProvider;
  89. protected DependencyResolver dependencyResolver;
  90. protected PluginLoader pluginLoader;
  91. protected boolean exactVersionAllowed = false;
  92. protected VersionManager versionManager;
  93. /**
  94. * The plugins roots are supplied as comma-separated list by {@code System.getProperty("pf4j.pluginsDir", "plugins")}.
  95. */
  96. protected AbstractPluginManager() {
  97. initialize();
  98. }
  99. /**
  100. * Constructs {@code AbstractPluginManager} with the given plugins roots.
  101. *
  102. * @param pluginsRoots the roots to search for plugins
  103. */
  104. protected AbstractPluginManager(Path... pluginsRoots) {
  105. this(Arrays.asList(pluginsRoots));
  106. }
  107. /**
  108. * Constructs {@code AbstractPluginManager} with the given plugins roots.
  109. *
  110. * @param pluginsRoots the roots to search for plugins
  111. */
  112. protected AbstractPluginManager(List<Path> pluginsRoots) {
  113. this.pluginsRoots.addAll(pluginsRoots);
  114. initialize();
  115. }
  116. @Override
  117. public void setSystemVersion(String version) {
  118. systemVersion = version;
  119. }
  120. @Override
  121. public String getSystemVersion() {
  122. return systemVersion;
  123. }
  124. /**
  125. * Returns a copy of plugins.
  126. */
  127. @Override
  128. public List<PluginWrapper> getPlugins() {
  129. return new ArrayList<>(plugins.values());
  130. }
  131. /**
  132. * Returns a copy of plugins with that state.
  133. */
  134. @Override
  135. public List<PluginWrapper> getPlugins(PluginState pluginState) {
  136. return getPlugins().stream()
  137. .filter(plugin -> pluginState.equals(plugin.getPluginState()))
  138. .collect(Collectors.toList());
  139. }
  140. @Override
  141. public List<PluginWrapper> getResolvedPlugins() {
  142. return resolvedPlugins;
  143. }
  144. @Override
  145. public List<PluginWrapper> getUnresolvedPlugins() {
  146. return unresolvedPlugins;
  147. }
  148. @Override
  149. public List<PluginWrapper> getStartedPlugins() {
  150. return startedPlugins;
  151. }
  152. @Override
  153. public PluginWrapper getPlugin(String pluginId) {
  154. return plugins.get(pluginId);
  155. }
  156. /**
  157. * Load a plugin.
  158. *
  159. * @param pluginPath the plugin location
  160. * @return the pluginId of the loaded plugin as specified in its {@linkplain PluginDescriptor metadata}
  161. * @throws IllegalArgumentException if the plugin location does not exist
  162. * @throws PluginRuntimeException if something goes wrong
  163. */
  164. @Override
  165. public String loadPlugin(Path pluginPath) {
  166. if ((pluginPath == null) || Files.notExists(pluginPath)) {
  167. throw new IllegalArgumentException(String.format("Specified plugin %s does not exist!", pluginPath));
  168. }
  169. log.debug("Loading plugin from '{}'", pluginPath);
  170. PluginWrapper pluginWrapper = loadPluginFromPath(pluginPath);
  171. // try to resolve the loaded plugin together with other possible plugins that depend on this plugin
  172. resolvePlugins();
  173. return pluginWrapper.getDescriptor().getPluginId();
  174. }
  175. /**
  176. * Load plugins.
  177. */
  178. @Override
  179. public void loadPlugins() {
  180. log.debug("Lookup plugins in '{}'", pluginsRoots);
  181. // check for plugins roots
  182. if (pluginsRoots.isEmpty()) {
  183. log.warn("No plugins roots configured");
  184. return;
  185. }
  186. pluginsRoots.forEach(path -> {
  187. if (Files.notExists(path) || !Files.isDirectory(path)) {
  188. log.warn("No '{}' root", path);
  189. }
  190. });
  191. // get all plugin paths from repository
  192. List<Path> pluginPaths = pluginRepository.getPluginPaths();
  193. // check for no plugins
  194. if (pluginPaths.isEmpty()) {
  195. log.info("No plugins");
  196. return;
  197. }
  198. log.debug("Found {} possible plugins: {}", pluginPaths.size(), pluginPaths);
  199. // load plugins from plugin paths
  200. for (Path pluginPath : pluginPaths) {
  201. try {
  202. loadPluginFromPath(pluginPath);
  203. } catch (PluginRuntimeException e) {
  204. log.error("Cannot load plugin '{}'", pluginPath, e);
  205. }
  206. }
  207. resolvePlugins();
  208. }
  209. /**
  210. * Unload all plugins
  211. */
  212. @Override
  213. public void unloadPlugins() {
  214. // wrap resolvedPlugins in new list because of concurrent modification
  215. for (PluginWrapper pluginWrapper : new ArrayList<>(resolvedPlugins)) {
  216. unloadPlugin(pluginWrapper.getPluginId());
  217. }
  218. }
  219. /**
  220. * Unload the specified plugin and it's dependents.
  221. *
  222. * @param pluginId the pluginId of the plugin to unload
  223. * @return true if the plugin was unloaded, otherwise false
  224. */
  225. @Override
  226. public boolean unloadPlugin(String pluginId) {
  227. return unloadPlugin(pluginId, true);
  228. }
  229. /**
  230. * Unload the specified plugin and it's dependents.
  231. *
  232. * @param pluginId the pluginId of the plugin to unload
  233. * @param unloadDependents if true, unload dependents
  234. * @return true if the plugin was unloaded, otherwise false
  235. */
  236. protected boolean unloadPlugin(String pluginId, boolean unloadDependents) {
  237. try {
  238. if (unloadDependents) {
  239. List<String> dependents = dependencyResolver.getDependents(pluginId);
  240. while (!dependents.isEmpty()) {
  241. String dependent = dependents.remove(0);
  242. unloadPlugin(dependent, false);
  243. dependents.addAll(0, dependencyResolver.getDependents(dependent));
  244. }
  245. }
  246. PluginWrapper pluginWrapper = getPlugin(pluginId);
  247. PluginState pluginState;
  248. try {
  249. pluginState = stopPlugin(pluginId, false);
  250. if (PluginState.STARTED == pluginState) {
  251. return false;
  252. }
  253. log.info("Unload plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
  254. } catch (Exception e) {
  255. if (pluginWrapper == null) {
  256. return false;
  257. }
  258. pluginState = PluginState.FAILED;
  259. }
  260. // remove the plugin
  261. plugins.remove(pluginId);
  262. getResolvedPlugins().remove(pluginWrapper);
  263. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  264. // remove the classloader
  265. Map<String, ClassLoader> pluginClassLoaders = getPluginClassLoaders();
  266. if (pluginClassLoaders.containsKey(pluginId)) {
  267. ClassLoader classLoader = pluginClassLoaders.remove(pluginId);
  268. if (classLoader instanceof Closeable) {
  269. try {
  270. ((Closeable) classLoader).close();
  271. } catch (IOException e) {
  272. throw new PluginRuntimeException(e, "Cannot close classloader");
  273. }
  274. }
  275. }
  276. return true;
  277. } catch (IllegalArgumentException e) {
  278. // ignore not found exceptions because this method is recursive
  279. }
  280. return false;
  281. }
  282. @Override
  283. public boolean deletePlugin(String pluginId) {
  284. checkPluginId(pluginId);
  285. PluginWrapper pluginWrapper = getPlugin(pluginId);
  286. // stop the plugin if it's started
  287. PluginState pluginState = stopPlugin(pluginId);
  288. if (PluginState.STARTED == pluginState) {
  289. log.error("Failed to stop plugin '{}' on delete", pluginId);
  290. return false;
  291. }
  292. // get an instance of plugin before the plugin is unloaded
  293. // for reason see https://github.com/pf4j/pf4j/issues/309
  294. Plugin plugin = pluginWrapper.getPlugin();
  295. if (!unloadPlugin(pluginId)) {
  296. log.error("Failed to unload plugin '{}' on delete", pluginId);
  297. return false;
  298. }
  299. // notify the plugin as it's deleted
  300. plugin.delete();
  301. return pluginRepository.deletePluginPath(pluginWrapper.getPluginPath());
  302. }
  303. /**
  304. * Start all active plugins.
  305. */
  306. @Override
  307. public void startPlugins() {
  308. for (PluginWrapper pluginWrapper : resolvedPlugins) {
  309. PluginState pluginState = pluginWrapper.getPluginState();
  310. if ((PluginState.DISABLED != pluginState) && (PluginState.STARTED != pluginState)) {
  311. try {
  312. log.info("Start plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
  313. pluginWrapper.getPlugin().start();
  314. pluginWrapper.setPluginState(PluginState.STARTED);
  315. pluginWrapper.setFailedException(null);
  316. startedPlugins.add(pluginWrapper);
  317. } catch (Exception | LinkageError e) {
  318. pluginWrapper.setPluginState(PluginState.FAILED);
  319. pluginWrapper.setFailedException(e);
  320. log.error("Unable to start plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()), e);
  321. } finally {
  322. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  323. }
  324. }
  325. }
  326. }
  327. /**
  328. * Start the specified plugin and its dependencies.
  329. */
  330. @Override
  331. public PluginState startPlugin(String pluginId) {
  332. checkPluginId(pluginId);
  333. PluginWrapper pluginWrapper = getPlugin(pluginId);
  334. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  335. PluginState pluginState = pluginWrapper.getPluginState();
  336. if (PluginState.STARTED == pluginState) {
  337. log.debug("Already started plugin '{}'", getPluginLabel(pluginDescriptor));
  338. return PluginState.STARTED;
  339. }
  340. if (!resolvedPlugins.contains(pluginWrapper)) {
  341. log.warn("Cannot start an unresolved plugin '{}'", getPluginLabel(pluginDescriptor));
  342. return pluginState;
  343. }
  344. if (PluginState.DISABLED == pluginState) {
  345. // automatically enable plugin on manual plugin start
  346. if (!enablePlugin(pluginId)) {
  347. return pluginState;
  348. }
  349. }
  350. for (PluginDependency dependency : pluginDescriptor.getDependencies()) {
  351. // start dependency only if it marked as required (non-optional) or if it optional and loaded
  352. if (!dependency.isOptional() || plugins.containsKey(dependency.getPluginId())) {
  353. startPlugin(dependency.getPluginId());
  354. }
  355. }
  356. log.info("Start plugin '{}'", getPluginLabel(pluginDescriptor));
  357. pluginWrapper.getPlugin().start();
  358. pluginWrapper.setPluginState(PluginState.STARTED);
  359. startedPlugins.add(pluginWrapper);
  360. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  361. return pluginWrapper.getPluginState();
  362. }
  363. /**
  364. * Stop all active plugins.
  365. */
  366. @Override
  367. public void stopPlugins() {
  368. // stop started plugins in reverse order
  369. Collections.reverse(startedPlugins);
  370. Iterator<PluginWrapper> itr = startedPlugins.iterator();
  371. while (itr.hasNext()) {
  372. PluginWrapper pluginWrapper = itr.next();
  373. PluginState pluginState = pluginWrapper.getPluginState();
  374. if (PluginState.STARTED == pluginState) {
  375. try {
  376. log.info("Stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
  377. pluginWrapper.getPlugin().stop();
  378. pluginWrapper.setPluginState(PluginState.STOPPED);
  379. itr.remove();
  380. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  381. } catch (PluginRuntimeException e) {
  382. log.error(e.getMessage(), e);
  383. }
  384. } else {
  385. // do nothing
  386. log.debug("Plugin '{}' is not started, nothing to stop", getPluginLabel(pluginWrapper.getDescriptor()));
  387. }
  388. }
  389. }
  390. /**
  391. * Stop the specified plugin and it's dependents.
  392. */
  393. @Override
  394. public PluginState stopPlugin(String pluginId) {
  395. return stopPlugin(pluginId, true);
  396. }
  397. /**
  398. * Stop the specified plugin and it's dependents.
  399. *
  400. * @param pluginId the pluginId of the plugin to stop
  401. * @param stopDependents if true, stop dependents
  402. * @return the plugin state after stopping
  403. */
  404. protected PluginState stopPlugin(String pluginId, boolean stopDependents) {
  405. checkPluginId(pluginId);
  406. // test for started plugin
  407. if (!checkPluginState(pluginId, PluginState.STARTED)) {
  408. // do nothing
  409. log.debug("Plugin '{}' is not started, nothing to stop", getPluginLabel(pluginId));
  410. return getPlugin(pluginId).getPluginState();
  411. }
  412. if (stopDependents) {
  413. List<String> dependents = dependencyResolver.getDependents(pluginId);
  414. while (!dependents.isEmpty()) {
  415. String dependent = dependents.remove(0);
  416. stopPlugin(dependent, false);
  417. dependents.addAll(0, dependencyResolver.getDependents(dependent));
  418. }
  419. }
  420. log.info("Stop plugin '{}'", getPluginLabel(pluginId));
  421. PluginWrapper pluginWrapper = getPlugin(pluginId);
  422. pluginWrapper.getPlugin().stop();
  423. pluginWrapper.setPluginState(PluginState.STOPPED);
  424. getStartedPlugins().remove(pluginWrapper);
  425. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, PluginState.STOPPED));
  426. return PluginState.STOPPED;
  427. }
  428. /**
  429. * Check if the plugin exists in the list of plugins.
  430. *
  431. * @param pluginId the pluginId to check
  432. * @throws IllegalArgumentException if the plugin does not exist
  433. */
  434. protected void checkPluginId(String pluginId) {
  435. if (!plugins.containsKey(pluginId)) {
  436. throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
  437. }
  438. }
  439. /**
  440. * Check if the plugin state is equals with the value passed for parameter {@code pluginState}.
  441. *
  442. * @param pluginId the pluginId to check
  443. * @return {@code true} if the plugin state is equals with the value passed for parameter {@code pluginState}, otherwise {@code false}
  444. */
  445. protected boolean checkPluginState(String pluginId, PluginState pluginState) {
  446. return getPlugin(pluginId).getPluginState() == pluginState;
  447. }
  448. @Override
  449. public boolean disablePlugin(String pluginId) {
  450. checkPluginId(pluginId);
  451. PluginWrapper pluginWrapper = getPlugin(pluginId);
  452. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  453. PluginState pluginState = pluginWrapper.getPluginState();
  454. if (PluginState.DISABLED == pluginState) {
  455. log.debug("Already disabled plugin '{}'", getPluginLabel(pluginDescriptor));
  456. return true;
  457. } else if (PluginState.STARTED == pluginState) {
  458. if (PluginState.STOPPED == stopPlugin(pluginId)) {
  459. log.error("Failed to stop plugin '{}' on disable", getPluginLabel(pluginDescriptor));
  460. return false;
  461. }
  462. }
  463. pluginWrapper.setPluginState(PluginState.DISABLED);
  464. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  465. pluginStatusProvider.disablePlugin(pluginId);
  466. log.info("Disabled plugin '{}'", getPluginLabel(pluginDescriptor));
  467. return true;
  468. }
  469. @Override
  470. public boolean enablePlugin(String pluginId) {
  471. checkPluginId(pluginId);
  472. PluginWrapper pluginWrapper = getPlugin(pluginId);
  473. if (!isPluginValid(pluginWrapper)) {
  474. log.warn("Plugin '{}' can not be enabled", getPluginLabel(pluginWrapper.getDescriptor()));
  475. return false;
  476. }
  477. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  478. PluginState pluginState = pluginWrapper.getPluginState();
  479. if (PluginState.DISABLED != pluginState) {
  480. log.debug("Plugin '{}' is not disabled", getPluginLabel(pluginDescriptor));
  481. return true;
  482. }
  483. pluginStatusProvider.enablePlugin(pluginId);
  484. pluginWrapper.setPluginState(PluginState.CREATED);
  485. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  486. log.info("Enabled plugin '{}'", getPluginLabel(pluginDescriptor));
  487. return true;
  488. }
  489. /**
  490. * Get the {@link ClassLoader} for plugin.
  491. */
  492. @Override
  493. public ClassLoader getPluginClassLoader(String pluginId) {
  494. return pluginClassLoaders.get(pluginId);
  495. }
  496. @SuppressWarnings("rawtypes")
  497. @Override
  498. public List<Class<?>> getExtensionClasses(String pluginId) {
  499. List<ExtensionWrapper> extensionsWrapper = extensionFinder.find(pluginId);
  500. List<Class<?>> extensionClasses = new ArrayList<>(extensionsWrapper.size());
  501. for (ExtensionWrapper extensionWrapper : extensionsWrapper) {
  502. Class<?> c = extensionWrapper.getDescriptor().extensionClass;
  503. extensionClasses.add(c);
  504. }
  505. return extensionClasses;
  506. }
  507. @Override
  508. public <T> List<Class<? extends T>> getExtensionClasses(Class<T> type) {
  509. return getExtensionClasses(extensionFinder.find(type));
  510. }
  511. @Override
  512. public <T> List<Class<? extends T>> getExtensionClasses(Class<T> type, String pluginId) {
  513. return getExtensionClasses(extensionFinder.find(type, pluginId));
  514. }
  515. @Override
  516. public <T> List<T> getExtensions(Class<T> type) {
  517. return getExtensions(extensionFinder.find(type));
  518. }
  519. @Override
  520. public <T> List<T> getExtensions(Class<T> type, String pluginId) {
  521. return getExtensions(extensionFinder.find(type, pluginId));
  522. }
  523. @Override
  524. @SuppressWarnings("unchecked")
  525. public List getExtensions(String pluginId) {
  526. List<ExtensionWrapper> extensionsWrapper = extensionFinder.find(pluginId);
  527. List extensions = new ArrayList<>(extensionsWrapper.size());
  528. for (ExtensionWrapper extensionWrapper : extensionsWrapper) {
  529. try {
  530. extensions.add(extensionWrapper.getExtension());
  531. } catch (PluginRuntimeException e) {
  532. log.error("Cannot retrieve extension", e);
  533. }
  534. }
  535. return extensions;
  536. }
  537. @Override
  538. public Set<String> getExtensionClassNames(String pluginId) {
  539. return extensionFinder.findClassNames(pluginId);
  540. }
  541. @Override
  542. public ExtensionFactory getExtensionFactory() {
  543. return extensionFactory;
  544. }
  545. public PluginLoader getPluginLoader() {
  546. return pluginLoader;
  547. }
  548. @Override
  549. public Path getPluginsRoot() {
  550. return pluginsRoots.stream()
  551. .findFirst()
  552. .orElseThrow(() -> new IllegalStateException("pluginsRoots have not been initialized, yet."));
  553. }
  554. public List<Path> getPluginsRoots() {
  555. return Collections.unmodifiableList(pluginsRoots);
  556. }
  557. @Override
  558. public RuntimeMode getRuntimeMode() {
  559. if (runtimeMode == null) {
  560. // retrieves the runtime mode from system
  561. String modeAsString = System.getProperty(MODE_PROPERTY_NAME, RuntimeMode.DEPLOYMENT.toString());
  562. runtimeMode = RuntimeMode.byName(modeAsString);
  563. }
  564. return runtimeMode;
  565. }
  566. @Override
  567. public PluginWrapper whichPlugin(Class<?> clazz) {
  568. ClassLoader classLoader = clazz.getClassLoader();
  569. for (PluginWrapper plugin : resolvedPlugins) {
  570. if (plugin.getPluginClassLoader() == classLoader) {
  571. return plugin;
  572. }
  573. }
  574. return null;
  575. }
  576. @Override
  577. public synchronized void addPluginStateListener(PluginStateListener listener) {
  578. pluginStateListeners.add(listener);
  579. }
  580. @Override
  581. public synchronized void removePluginStateListener(PluginStateListener listener) {
  582. pluginStateListeners.remove(listener);
  583. }
  584. public String getVersion() {
  585. return Pf4jInfo.VERSION;
  586. }
  587. protected abstract PluginRepository createPluginRepository();
  588. protected abstract PluginFactory createPluginFactory();
  589. protected abstract ExtensionFactory createExtensionFactory();
  590. protected abstract PluginDescriptorFinder createPluginDescriptorFinder();
  591. protected abstract ExtensionFinder createExtensionFinder();
  592. protected abstract PluginStatusProvider createPluginStatusProvider();
  593. protected abstract PluginLoader createPluginLoader();
  594. protected abstract VersionManager createVersionManager();
  595. protected PluginDescriptorFinder getPluginDescriptorFinder() {
  596. return pluginDescriptorFinder;
  597. }
  598. protected PluginFactory getPluginFactory() {
  599. return pluginFactory;
  600. }
  601. protected Map<String, ClassLoader> getPluginClassLoaders() {
  602. return pluginClassLoaders;
  603. }
  604. protected void initialize() {
  605. plugins = new HashMap<>();
  606. pluginClassLoaders = new HashMap<>();
  607. unresolvedPlugins = new ArrayList<>();
  608. resolvedPlugins = new ArrayList<>();
  609. startedPlugins = new ArrayList<>();
  610. pluginStateListeners = new ArrayList<>();
  611. if (pluginsRoots.isEmpty()) {
  612. pluginsRoots.addAll(createPluginsRoot());
  613. }
  614. pluginRepository = createPluginRepository();
  615. pluginFactory = createPluginFactory();
  616. extensionFactory = createExtensionFactory();
  617. pluginDescriptorFinder = createPluginDescriptorFinder();
  618. extensionFinder = createExtensionFinder();
  619. pluginStatusProvider = createPluginStatusProvider();
  620. pluginLoader = createPluginLoader();
  621. versionManager = createVersionManager();
  622. dependencyResolver = new DependencyResolver(versionManager);
  623. }
  624. /**
  625. * Add the possibility to override the plugins roots.
  626. * If a {@link #PLUGINS_DIR_PROPERTY_NAME} system property is defined than this method returns that roots.
  627. * If {@link #getRuntimeMode()} returns {@link RuntimeMode#DEVELOPMENT} than {@link #DEVELOPMENT_PLUGINS_DIR}
  628. * is returned else this method returns {@link #DEFAULT_PLUGINS_DIR}.
  629. *
  630. * @return the plugins root
  631. */
  632. protected List<Path> createPluginsRoot() {
  633. String pluginsDir = System.getProperty(PLUGINS_DIR_PROPERTY_NAME);
  634. if (pluginsDir != null && !pluginsDir.isEmpty()) {
  635. return Arrays.stream(pluginsDir.split(","))
  636. .map(String::trim)
  637. .map(Paths::get)
  638. .collect(Collectors.toList());
  639. }
  640. pluginsDir = isDevelopment() ? DEVELOPMENT_PLUGINS_DIR : DEFAULT_PLUGINS_DIR;
  641. return Collections.singletonList(Paths.get(pluginsDir));
  642. }
  643. /**
  644. * Check if this plugin is valid (satisfies "requires" param) for a given system version.
  645. *
  646. * @param pluginWrapper the plugin to check
  647. * @return true if plugin satisfies the "requires" or if requires was left blank
  648. */
  649. protected boolean isPluginValid(PluginWrapper pluginWrapper) {
  650. String requires = pluginWrapper.getDescriptor().getRequires().trim();
  651. if (!isExactVersionAllowed() && requires.matches("^\\d+\\.\\d+\\.\\d+$")) {
  652. // If exact versions are not allowed in requires, rewrite to >= expression
  653. requires = ">=" + requires;
  654. }
  655. if (systemVersion.equals("0.0.0") || versionManager.checkVersionConstraint(systemVersion, requires)) {
  656. return true;
  657. }
  658. log.warn("Plugin '{}' requires a minimum system version of {}, and you have {}",
  659. getPluginLabel(pluginWrapper.getDescriptor()),
  660. requires,
  661. getSystemVersion());
  662. return false;
  663. }
  664. /**
  665. * Check if the plugin is disabled.
  666. *
  667. * @param pluginId the pluginId to check
  668. * @return true if the plugin is disabled, otherwise false
  669. */
  670. protected boolean isPluginDisabled(String pluginId) {
  671. return pluginStatusProvider.isPluginDisabled(pluginId);
  672. }
  673. /**
  674. * It resolves the plugins by checking the dependencies.
  675. * It also checks for cyclic dependencies, missing dependencies and wrong versions of the dependencies.
  676. *
  677. * @throws PluginRuntimeException if something goes wrong
  678. */
  679. protected void resolvePlugins() {
  680. // retrieves the plugins descriptors
  681. List<PluginDescriptor> descriptors = plugins.values().stream()
  682. .map(PluginWrapper::getDescriptor)
  683. .collect(Collectors.toList());
  684. DependencyResolver.Result result = dependencyResolver.resolve(descriptors);
  685. if (result.hasCyclicDependency()) {
  686. throw new DependencyResolver.CyclicDependencyException();
  687. }
  688. List<String> notFoundDependencies = result.getNotFoundDependencies();
  689. if (!notFoundDependencies.isEmpty()) {
  690. throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies);
  691. }
  692. List<DependencyResolver.WrongDependencyVersion> wrongVersionDependencies = result.getWrongVersionDependencies();
  693. if (!wrongVersionDependencies.isEmpty()) {
  694. throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies);
  695. }
  696. List<String> sortedPlugins = result.getSortedPlugins();
  697. // move plugins from "unresolved" to "resolved"
  698. for (String pluginId : sortedPlugins) {
  699. PluginWrapper pluginWrapper = plugins.get(pluginId);
  700. if (unresolvedPlugins.remove(pluginWrapper)) {
  701. PluginState pluginState = pluginWrapper.getPluginState();
  702. if (pluginState != PluginState.DISABLED) {
  703. pluginWrapper.setPluginState(PluginState.RESOLVED);
  704. }
  705. resolvedPlugins.add(pluginWrapper);
  706. log.info("Plugin '{}' resolved", getPluginLabel(pluginWrapper.getDescriptor()));
  707. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  708. }
  709. }
  710. }
  711. /**
  712. * Fire a plugin state event.
  713. * This method is called when a plugin is loaded, started, stopped, etc.
  714. *
  715. * @param event the plugin state event
  716. */
  717. protected synchronized void firePluginStateEvent(PluginStateEvent event) {
  718. if (event.getPluginState() == event.getOldState()) {
  719. // ignore events without state change
  720. return;
  721. }
  722. for (PluginStateListener listener : pluginStateListeners) {
  723. log.trace("Fire '{}' to '{}'", event, listener);
  724. listener.pluginStateChanged(event);
  725. }
  726. }
  727. /**
  728. * Load the plugin from the specified path.
  729. *
  730. * @param pluginPath the path to the plugin
  731. * @return the loaded plugin
  732. * @throws PluginAlreadyLoadedException if the plugin is already loaded
  733. * @throws InvalidPluginDescriptorException if the plugin is invalid
  734. */
  735. protected PluginWrapper loadPluginFromPath(Path pluginPath) {
  736. // Test for plugin path duplication
  737. String pluginId = idForPath(pluginPath);
  738. if (pluginId != null) {
  739. throw new PluginAlreadyLoadedException(pluginId, pluginPath);
  740. }
  741. // Retrieve and validate the plugin descriptor
  742. PluginDescriptorFinder pluginDescriptorFinder = getPluginDescriptorFinder();
  743. log.debug("Use '{}' to find plugins descriptors", pluginDescriptorFinder);
  744. log.debug("Finding plugin descriptor for plugin '{}'", pluginPath);
  745. PluginDescriptor pluginDescriptor = pluginDescriptorFinder.find(pluginPath);
  746. validatePluginDescriptor(pluginDescriptor);
  747. // Check there are no loaded plugins with the retrieved id
  748. pluginId = pluginDescriptor.getPluginId();
  749. if (plugins.containsKey(pluginId)) {
  750. PluginWrapper loadedPlugin = getPlugin(pluginId);
  751. throw new PluginRuntimeException("There is an already loaded plugin ({}) "
  752. + "with the same id ({}) as the plugin at path '{}'. Simultaneous loading "
  753. + "of plugins with the same PluginId is not currently supported.\n"
  754. + "As a workaround you may include PluginVersion and PluginProvider "
  755. + "in PluginId.",
  756. loadedPlugin, pluginId, pluginPath);
  757. }
  758. log.debug("Found descriptor {}", pluginDescriptor);
  759. String pluginClassName = pluginDescriptor.getPluginClass();
  760. log.debug("Class '{}' for plugin '{}'", pluginClassName, pluginPath);
  761. // load plugin
  762. log.debug("Loading plugin '{}'", pluginPath);
  763. ClassLoader pluginClassLoader = getPluginLoader().loadPlugin(pluginPath, pluginDescriptor);
  764. log.debug("Loaded plugin '{}' with class loader '{}'", pluginPath, pluginClassLoader);
  765. PluginWrapper pluginWrapper = createPluginWrapper(pluginDescriptor, pluginPath, pluginClassLoader);
  766. // test for disabled plugin
  767. if (isPluginDisabled(pluginDescriptor.getPluginId())) {
  768. log.info("Plugin '{}' is disabled", pluginPath);
  769. pluginWrapper.setPluginState(PluginState.DISABLED);
  770. }
  771. // validate the plugin
  772. if (!isPluginValid(pluginWrapper)) {
  773. log.warn("Plugin '{}' is invalid and it will be disabled", pluginPath);
  774. pluginWrapper.setPluginState(PluginState.DISABLED);
  775. }
  776. log.debug("Created wrapper '{}' for plugin '{}'", pluginWrapper, pluginPath);
  777. pluginId = pluginDescriptor.getPluginId();
  778. // add plugin to the list with plugins
  779. plugins.put(pluginId, pluginWrapper);
  780. getUnresolvedPlugins().add(pluginWrapper);
  781. // add plugin class loader to the list with class loaders
  782. getPluginClassLoaders().put(pluginId, pluginClassLoader);
  783. return pluginWrapper;
  784. }
  785. /**
  786. * Creates the plugin wrapper.
  787. * <p>
  788. * Override this if you want to prevent plugins having full access to the plugin manager.
  789. *
  790. * @param pluginDescriptor the plugin descriptor
  791. * @param pluginPath the path to the plugin
  792. * @param pluginClassLoader the class loader for the plugin
  793. * @return the plugin wrapper
  794. */
  795. protected PluginWrapper createPluginWrapper(PluginDescriptor pluginDescriptor, Path pluginPath, ClassLoader pluginClassLoader) {
  796. // create the plugin wrapper
  797. log.debug("Creating wrapper for plugin '{}'", pluginPath);
  798. PluginWrapper pluginWrapper = new PluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader);
  799. pluginWrapper.setPluginFactory(getPluginFactory());
  800. return pluginWrapper;
  801. }
  802. /**
  803. * Tests for already loaded plugins on given path.
  804. *
  805. * @param pluginPath the path to investigate
  806. * @return id of plugin or null if not loaded
  807. */
  808. protected String idForPath(Path pluginPath) {
  809. for (PluginWrapper plugin : plugins.values()) {
  810. if (plugin.getPluginPath().equals(pluginPath)) {
  811. return plugin.getPluginId();
  812. }
  813. }
  814. return null;
  815. }
  816. /**
  817. * Override this to change the validation criteria.
  818. *
  819. * @param descriptor the plugin descriptor to validate
  820. * @throws InvalidPluginDescriptorException if validation fails
  821. */
  822. protected void validatePluginDescriptor(PluginDescriptor descriptor) {
  823. if (StringUtils.isNullOrEmpty(descriptor.getPluginId())) {
  824. throw new InvalidPluginDescriptorException("Field 'id' cannot be empty");
  825. }
  826. if (descriptor.getVersion() == null) {
  827. throw new InvalidPluginDescriptorException("Field 'version' cannot be empty");
  828. }
  829. }
  830. /**
  831. * Check if the exact version in requires is allowed.
  832. *
  833. * @return true if exact versions in requires is allowed
  834. */
  835. public boolean isExactVersionAllowed() {
  836. return exactVersionAllowed;
  837. }
  838. /**
  839. * Set to true to allow requires expression to be exactly x.y.z.
  840. * The default is false, meaning that using an exact version x.y.z will
  841. * implicitly mean the same as &gt;=x.y.z
  842. *
  843. * @param exactVersionAllowed set to true or false
  844. */
  845. public void setExactVersionAllowed(boolean exactVersionAllowed) {
  846. this.exactVersionAllowed = exactVersionAllowed;
  847. }
  848. @Override
  849. public VersionManager getVersionManager() {
  850. return versionManager;
  851. }
  852. /**
  853. * The plugin label is used in logging, and it's a string in format {@code pluginId@pluginVersion}.
  854. *
  855. * @param pluginDescriptor the plugin descriptor
  856. * @return the plugin label
  857. */
  858. protected String getPluginLabel(PluginDescriptor pluginDescriptor) {
  859. return pluginDescriptor.getPluginId() + "@" + pluginDescriptor.getVersion();
  860. }
  861. /**
  862. * Shortcut for {@code getPluginLabel(getPlugin(pluginId).getDescriptor())}.
  863. *
  864. * @param pluginId the pluginId
  865. * @return the plugin label
  866. */
  867. protected String getPluginLabel(String pluginId) {
  868. return getPluginLabel(getPlugin(pluginId).getDescriptor());
  869. }
  870. @SuppressWarnings("unchecked")
  871. protected <T> List<Class<? extends T>> getExtensionClasses(List<ExtensionWrapper<T>> extensionsWrapper) {
  872. List<Class<? extends T>> extensionClasses = new ArrayList<>(extensionsWrapper.size());
  873. for (ExtensionWrapper<T> extensionWrapper : extensionsWrapper) {
  874. Class<T> c = (Class<T>) extensionWrapper.getDescriptor().extensionClass;
  875. extensionClasses.add(c);
  876. }
  877. return extensionClasses;
  878. }
  879. protected <T> List<T> getExtensions(List<ExtensionWrapper<T>> extensionsWrapper) {
  880. List<T> extensions = new ArrayList<>(extensionsWrapper.size());
  881. for (ExtensionWrapper<T> extensionWrapper : extensionsWrapper) {
  882. try {
  883. extensions.add(extensionWrapper.getExtension());
  884. } catch (PluginRuntimeException e) {
  885. log.error("Cannot retrieve extension", e);
  886. }
  887. }
  888. return extensions;
  889. }
  890. }