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

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