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

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