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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049
  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 wont 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(e.getMessage(), e);
  205. }
  206. }
  207. // resolve plugins
  208. try {
  209. resolvePlugins();
  210. } catch (PluginRuntimeException e) {
  211. log.error(e.getMessage(), e);
  212. }
  213. }
  214. /**
  215. * Unload all plugins
  216. */
  217. @Override
  218. public void unloadPlugins() {
  219. // wrap resolvedPlugins in new list because of concurrent modification
  220. for (PluginWrapper pluginWrapper : new ArrayList<>(resolvedPlugins)) {
  221. unloadPlugin(pluginWrapper.getPluginId());
  222. }
  223. }
  224. /**
  225. * Unload the specified plugin and it's dependents.
  226. *
  227. * @param pluginId the pluginId of the plugin to unload
  228. * @return true if the plugin was unloaded, otherwise false
  229. */
  230. @Override
  231. public boolean unloadPlugin(String pluginId) {
  232. return unloadPlugin(pluginId, true);
  233. }
  234. /**
  235. * Unload the specified plugin and it's dependents.
  236. *
  237. * @param pluginId the pluginId of the plugin to unload
  238. * @param unloadDependents if true, unload dependents
  239. * @return true if the plugin was unloaded, otherwise false
  240. */
  241. protected boolean unloadPlugin(String pluginId, boolean unloadDependents) {
  242. try {
  243. if (unloadDependents) {
  244. List<String> dependents = dependencyResolver.getDependents(pluginId);
  245. while (!dependents.isEmpty()) {
  246. String dependent = dependents.remove(0);
  247. unloadPlugin(dependent, false);
  248. dependents.addAll(0, dependencyResolver.getDependents(dependent));
  249. }
  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. if (pluginWrapper == null) {
  261. return false;
  262. }
  263. pluginState = PluginState.FAILED;
  264. }
  265. // remove the plugin
  266. plugins.remove(pluginId);
  267. getResolvedPlugins().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. return true;
  282. } catch (IllegalArgumentException e) {
  283. // ignore not found exceptions because this method is recursive
  284. }
  285. return false;
  286. }
  287. @Override
  288. public boolean deletePlugin(String pluginId) {
  289. checkPluginId(pluginId);
  290. PluginWrapper pluginWrapper = getPlugin(pluginId);
  291. // stop the plugin if it's started
  292. PluginState pluginState = stopPlugin(pluginId);
  293. if (PluginState.STARTED == pluginState) {
  294. log.error("Failed to stop plugin '{}' on delete", pluginId);
  295. return false;
  296. }
  297. // get an instance of plugin before the plugin is unloaded
  298. // for reason see https://github.com/pf4j/pf4j/issues/309
  299. Plugin plugin = pluginWrapper.getPlugin();
  300. if (!unloadPlugin(pluginId)) {
  301. log.error("Failed to unload plugin '{}' on delete", pluginId);
  302. return false;
  303. }
  304. // notify the plugin as it's deleted
  305. plugin.delete();
  306. return pluginRepository.deletePluginPath(pluginWrapper.getPluginPath());
  307. }
  308. /**
  309. * Start all active plugins.
  310. */
  311. @Override
  312. public void startPlugins() {
  313. for (PluginWrapper pluginWrapper : resolvedPlugins) {
  314. PluginState pluginState = pluginWrapper.getPluginState();
  315. if ((PluginState.DISABLED != pluginState) && (PluginState.STARTED != pluginState)) {
  316. try {
  317. log.info("Start plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
  318. pluginWrapper.getPlugin().start();
  319. pluginWrapper.setPluginState(PluginState.STARTED);
  320. pluginWrapper.setFailedException(null);
  321. startedPlugins.add(pluginWrapper);
  322. } catch (Exception | LinkageError e) {
  323. pluginWrapper.setPluginState(PluginState.FAILED);
  324. pluginWrapper.setFailedException(e);
  325. log.error("Unable to start plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()), e);
  326. } finally {
  327. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  328. }
  329. }
  330. }
  331. }
  332. /**
  333. * Start the specified plugin and its dependencies.
  334. */
  335. @Override
  336. public PluginState startPlugin(String pluginId) {
  337. checkPluginId(pluginId);
  338. PluginWrapper pluginWrapper = getPlugin(pluginId);
  339. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  340. PluginState pluginState = pluginWrapper.getPluginState();
  341. if (PluginState.STARTED == pluginState) {
  342. log.debug("Already started plugin '{}'", getPluginLabel(pluginDescriptor));
  343. return PluginState.STARTED;
  344. }
  345. if (!resolvedPlugins.contains(pluginWrapper)) {
  346. log.warn("Cannot start an unresolved plugin '{}'", getPluginLabel(pluginDescriptor));
  347. return pluginState;
  348. }
  349. if (PluginState.DISABLED == pluginState) {
  350. // automatically enable plugin on manual plugin start
  351. if (!enablePlugin(pluginId)) {
  352. return pluginState;
  353. }
  354. }
  355. for (PluginDependency dependency : pluginDescriptor.getDependencies()) {
  356. // start dependency only if it marked as required (non-optional) or if it optional and loaded
  357. if (!dependency.isOptional() || plugins.containsKey(dependency.getPluginId())) {
  358. startPlugin(dependency.getPluginId());
  359. }
  360. }
  361. log.info("Start plugin '{}'", getPluginLabel(pluginDescriptor));
  362. pluginWrapper.getPlugin().start();
  363. pluginWrapper.setPluginState(PluginState.STARTED);
  364. startedPlugins.add(pluginWrapper);
  365. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  366. return pluginWrapper.getPluginState();
  367. }
  368. /**
  369. * Stop all active plugins.
  370. */
  371. @Override
  372. public void stopPlugins() {
  373. // stop started plugins in reverse order
  374. Collections.reverse(startedPlugins);
  375. Iterator<PluginWrapper> itr = startedPlugins.iterator();
  376. while (itr.hasNext()) {
  377. PluginWrapper pluginWrapper = itr.next();
  378. PluginState pluginState = pluginWrapper.getPluginState();
  379. if (PluginState.STARTED == pluginState) {
  380. try {
  381. log.info("Stop plugin '{}'", getPluginLabel(pluginWrapper.getDescriptor()));
  382. pluginWrapper.getPlugin().stop();
  383. pluginWrapper.setPluginState(PluginState.STOPPED);
  384. itr.remove();
  385. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  386. } catch (PluginRuntimeException e) {
  387. log.error(e.getMessage(), e);
  388. }
  389. }
  390. }
  391. }
  392. /**
  393. * Stop the specified plugin and it's dependents.
  394. */
  395. @Override
  396. public PluginState stopPlugin(String pluginId) {
  397. return stopPlugin(pluginId, true);
  398. }
  399. /**
  400. * Stop the specified plugin and it's dependents.
  401. *
  402. * @param pluginId the pluginId of the plugin to stop
  403. * @param stopDependents if true, stop dependents
  404. * @return the plugin state after stopping
  405. */
  406. protected PluginState stopPlugin(String pluginId, boolean stopDependents) {
  407. checkPluginId(pluginId);
  408. PluginWrapper pluginWrapper = getPlugin(pluginId);
  409. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  410. PluginState pluginState = pluginWrapper.getPluginState();
  411. if (PluginState.STOPPED == pluginState) {
  412. log.debug("Already stopped plugin '{}'", getPluginLabel(pluginDescriptor));
  413. return PluginState.STOPPED;
  414. }
  415. // test for disabled plugin
  416. if (PluginState.DISABLED == pluginState) {
  417. // do nothing
  418. return pluginState;
  419. }
  420. if (stopDependents) {
  421. List<String> dependents = dependencyResolver.getDependents(pluginId);
  422. while (!dependents.isEmpty()) {
  423. String dependent = dependents.remove(0);
  424. stopPlugin(dependent, false);
  425. dependents.addAll(0, dependencyResolver.getDependents(dependent));
  426. }
  427. }
  428. log.info("Stop plugin '{}'", getPluginLabel(pluginDescriptor));
  429. pluginWrapper.getPlugin().stop();
  430. pluginWrapper.setPluginState(PluginState.STOPPED);
  431. startedPlugins.remove(pluginWrapper);
  432. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  433. return pluginWrapper.getPluginState();
  434. }
  435. /**
  436. * Check if the plugin exists in the list of plugins.
  437. *
  438. * @param pluginId the pluginId to check
  439. * @throws IllegalArgumentException if the plugin does not exist
  440. */
  441. protected void checkPluginId(String pluginId) {
  442. if (!plugins.containsKey(pluginId)) {
  443. throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
  444. }
  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. }
  456. if (PluginState.STOPPED == stopPlugin(pluginId)) {
  457. pluginWrapper.setPluginState(PluginState.DISABLED);
  458. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, PluginState.STOPPED));
  459. pluginStatusProvider.disablePlugin(pluginId);
  460. log.info("Disabled plugin '{}'", getPluginLabel(pluginDescriptor));
  461. return true;
  462. }
  463. return false;
  464. }
  465. @Override
  466. public boolean enablePlugin(String pluginId) {
  467. checkPluginId(pluginId);
  468. PluginWrapper pluginWrapper = getPlugin(pluginId);
  469. if (!isPluginValid(pluginWrapper)) {
  470. log.warn("Plugin '{}' can not be enabled", getPluginLabel(pluginWrapper.getDescriptor()));
  471. return false;
  472. }
  473. PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
  474. PluginState pluginState = pluginWrapper.getPluginState();
  475. if (PluginState.DISABLED != pluginState) {
  476. log.debug("Plugin '{}' is not disabled", getPluginLabel(pluginDescriptor));
  477. return true;
  478. }
  479. pluginStatusProvider.enablePlugin(pluginId);
  480. pluginWrapper.setPluginState(PluginState.CREATED);
  481. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  482. log.info("Enabled plugin '{}'", getPluginLabel(pluginDescriptor));
  483. return true;
  484. }
  485. /**
  486. * Get the {@link ClassLoader} for plugin.
  487. */
  488. @Override
  489. public ClassLoader getPluginClassLoader(String pluginId) {
  490. return pluginClassLoaders.get(pluginId);
  491. }
  492. @SuppressWarnings("rawtypes")
  493. @Override
  494. public List<Class<?>> getExtensionClasses(String pluginId) {
  495. List<ExtensionWrapper> extensionsWrapper = extensionFinder.find(pluginId);
  496. List<Class<?>> extensionClasses = new ArrayList<>(extensionsWrapper.size());
  497. for (ExtensionWrapper extensionWrapper : extensionsWrapper) {
  498. Class<?> c = extensionWrapper.getDescriptor().extensionClass;
  499. extensionClasses.add(c);
  500. }
  501. return extensionClasses;
  502. }
  503. @Override
  504. public <T> List<Class<? extends T>> getExtensionClasses(Class<T> type) {
  505. return getExtensionClasses(extensionFinder.find(type));
  506. }
  507. @Override
  508. public <T> List<Class<? extends T>> getExtensionClasses(Class<T> type, String pluginId) {
  509. return getExtensionClasses(extensionFinder.find(type, pluginId));
  510. }
  511. @Override
  512. public <T> List<T> getExtensions(Class<T> type) {
  513. return getExtensions(extensionFinder.find(type));
  514. }
  515. @Override
  516. public <T> List<T> getExtensions(Class<T> type, String pluginId) {
  517. return getExtensions(extensionFinder.find(type, pluginId));
  518. }
  519. @Override
  520. @SuppressWarnings("unchecked")
  521. public List getExtensions(String pluginId) {
  522. List<ExtensionWrapper> extensionsWrapper = extensionFinder.find(pluginId);
  523. List extensions = new ArrayList<>(extensionsWrapper.size());
  524. for (ExtensionWrapper extensionWrapper : extensionsWrapper) {
  525. try {
  526. extensions.add(extensionWrapper.getExtension());
  527. } catch (PluginRuntimeException e) {
  528. log.error("Cannot retrieve extension", e);
  529. }
  530. }
  531. return extensions;
  532. }
  533. @Override
  534. public Set<String> getExtensionClassNames(String pluginId) {
  535. return extensionFinder.findClassNames(pluginId);
  536. }
  537. @Override
  538. public ExtensionFactory getExtensionFactory() {
  539. return extensionFactory;
  540. }
  541. public PluginLoader getPluginLoader() {
  542. return pluginLoader;
  543. }
  544. @Override
  545. public Path getPluginsRoot() {
  546. return pluginsRoots.stream()
  547. .findFirst()
  548. .orElseThrow(() -> new IllegalStateException("pluginsRoots have not been initialized, yet."));
  549. }
  550. public List<Path> getPluginsRoots() {
  551. return Collections.unmodifiableList(pluginsRoots);
  552. }
  553. @Override
  554. public RuntimeMode getRuntimeMode() {
  555. if (runtimeMode == null) {
  556. // retrieves the runtime mode from system
  557. String modeAsString = System.getProperty(MODE_PROPERTY_NAME, RuntimeMode.DEPLOYMENT.toString());
  558. runtimeMode = RuntimeMode.byName(modeAsString);
  559. }
  560. return runtimeMode;
  561. }
  562. @Override
  563. public PluginWrapper whichPlugin(Class<?> clazz) {
  564. ClassLoader classLoader = clazz.getClassLoader();
  565. for (PluginWrapper plugin : resolvedPlugins) {
  566. if (plugin.getPluginClassLoader() == classLoader) {
  567. return plugin;
  568. }
  569. }
  570. return null;
  571. }
  572. @Override
  573. public synchronized void addPluginStateListener(PluginStateListener listener) {
  574. pluginStateListeners.add(listener);
  575. }
  576. @Override
  577. public synchronized void removePluginStateListener(PluginStateListener listener) {
  578. pluginStateListeners.remove(listener);
  579. }
  580. public String getVersion() {
  581. return Pf4jInfo.VERSION;
  582. }
  583. protected abstract PluginRepository createPluginRepository();
  584. protected abstract PluginFactory createPluginFactory();
  585. protected abstract ExtensionFactory createExtensionFactory();
  586. protected abstract PluginDescriptorFinder createPluginDescriptorFinder();
  587. protected abstract ExtensionFinder createExtensionFinder();
  588. protected abstract PluginStatusProvider createPluginStatusProvider();
  589. protected abstract PluginLoader createPluginLoader();
  590. protected abstract VersionManager createVersionManager();
  591. protected PluginDescriptorFinder getPluginDescriptorFinder() {
  592. return pluginDescriptorFinder;
  593. }
  594. protected PluginFactory getPluginFactory() {
  595. return pluginFactory;
  596. }
  597. protected Map<String, ClassLoader> getPluginClassLoaders() {
  598. return pluginClassLoaders;
  599. }
  600. protected void initialize() {
  601. plugins = new HashMap<>();
  602. pluginClassLoaders = new HashMap<>();
  603. unresolvedPlugins = new ArrayList<>();
  604. resolvedPlugins = new ArrayList<>();
  605. startedPlugins = new ArrayList<>();
  606. pluginStateListeners = new ArrayList<>();
  607. if (pluginsRoots.isEmpty()) {
  608. pluginsRoots.addAll(createPluginsRoot());
  609. }
  610. pluginRepository = createPluginRepository();
  611. pluginFactory = createPluginFactory();
  612. extensionFactory = createExtensionFactory();
  613. pluginDescriptorFinder = createPluginDescriptorFinder();
  614. extensionFinder = createExtensionFinder();
  615. pluginStatusProvider = createPluginStatusProvider();
  616. pluginLoader = createPluginLoader();
  617. versionManager = createVersionManager();
  618. dependencyResolver = new DependencyResolver(versionManager);
  619. }
  620. /**
  621. * Add the possibility to override the plugins roots.
  622. * If a {@link #PLUGINS_DIR_PROPERTY_NAME} system property is defined than this method returns that roots.
  623. * If {@link #getRuntimeMode()} returns {@link RuntimeMode#DEVELOPMENT} than {@link #DEVELOPMENT_PLUGINS_DIR}
  624. * is returned else this method returns {@link #DEFAULT_PLUGINS_DIR}.
  625. *
  626. * @return the plugins root
  627. */
  628. protected List<Path> createPluginsRoot() {
  629. String pluginsDir = System.getProperty(PLUGINS_DIR_PROPERTY_NAME);
  630. if (pluginsDir != null && !pluginsDir.isEmpty()) {
  631. return Arrays.stream(pluginsDir.split(","))
  632. .map(String::trim)
  633. .map(Paths::get)
  634. .collect(Collectors.toList());
  635. }
  636. pluginsDir = isDevelopment() ? DEVELOPMENT_PLUGINS_DIR : DEFAULT_PLUGINS_DIR;
  637. return Collections.singletonList(Paths.get(pluginsDir));
  638. }
  639. /**
  640. * Check if this plugin is valid (satisfies "requires" param) for a given system version.
  641. *
  642. * @param pluginWrapper the plugin to check
  643. * @return true if plugin satisfies the "requires" or if requires was left blank
  644. */
  645. protected boolean isPluginValid(PluginWrapper pluginWrapper) {
  646. String requires = pluginWrapper.getDescriptor().getRequires().trim();
  647. if (!isExactVersionAllowed() && requires.matches("^\\d+\\.\\d+\\.\\d+$")) {
  648. // If exact versions are not allowed in requires, rewrite to >= expression
  649. requires = ">=" + requires;
  650. }
  651. if (systemVersion.equals("0.0.0") || versionManager.checkVersionConstraint(systemVersion, requires)) {
  652. return true;
  653. }
  654. log.warn("Plugin '{}' requires a minimum system version of {}, and you have {}",
  655. getPluginLabel(pluginWrapper.getDescriptor()),
  656. requires,
  657. getSystemVersion());
  658. return false;
  659. }
  660. /**
  661. * Check if the plugin is disabled.
  662. *
  663. * @param pluginId the pluginId to check
  664. * @return true if the plugin is disabled, otherwise false
  665. */
  666. protected boolean isPluginDisabled(String pluginId) {
  667. return pluginStatusProvider.isPluginDisabled(pluginId);
  668. }
  669. /**
  670. * It resolves the plugins by checking the dependencies.
  671. * It also checks for cyclic dependencies, missing dependencies and wrong versions of the dependencies.
  672. *
  673. * @throws PluginRuntimeException if something goes wrong
  674. */
  675. protected void resolvePlugins() {
  676. // retrieves the plugins descriptors
  677. List<PluginDescriptor> descriptors = new ArrayList<>();
  678. for (PluginWrapper plugin : plugins.values()) {
  679. descriptors.add(plugin.getDescriptor());
  680. }
  681. DependencyResolver.Result result = dependencyResolver.resolve(descriptors);
  682. if (result.hasCyclicDependency()) {
  683. throw new DependencyResolver.CyclicDependencyException();
  684. }
  685. List<String> notFoundDependencies = result.getNotFoundDependencies();
  686. if (!notFoundDependencies.isEmpty()) {
  687. throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies);
  688. }
  689. List<DependencyResolver.WrongDependencyVersion> wrongVersionDependencies = result.getWrongVersionDependencies();
  690. if (!wrongVersionDependencies.isEmpty()) {
  691. throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies);
  692. }
  693. List<String> sortedPlugins = result.getSortedPlugins();
  694. // move plugins from "unresolved" to "resolved"
  695. for (String pluginId : sortedPlugins) {
  696. PluginWrapper pluginWrapper = plugins.get(pluginId);
  697. if (unresolvedPlugins.remove(pluginWrapper)) {
  698. PluginState pluginState = pluginWrapper.getPluginState();
  699. if (pluginState != PluginState.DISABLED) {
  700. pluginWrapper.setPluginState(PluginState.RESOLVED);
  701. }
  702. resolvedPlugins.add(pluginWrapper);
  703. log.info("Plugin '{}' resolved", getPluginLabel(pluginWrapper.getDescriptor()));
  704. firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
  705. }
  706. }
  707. }
  708. /**
  709. * Fire a plugin state event.
  710. * This method is called when a plugin is loaded, started, stopped, etc.
  711. *
  712. * @param event the plugin state event
  713. */
  714. protected synchronized void firePluginStateEvent(PluginStateEvent event) {
  715. for (PluginStateListener listener : pluginStateListeners) {
  716. log.trace("Fire '{}' to '{}'", event, listener);
  717. listener.pluginStateChanged(event);
  718. }
  719. }
  720. /**
  721. * Load the plugin from the specified path.
  722. *
  723. * @param pluginPath the path to the plugin
  724. * @return the loaded plugin
  725. * @throws PluginAlreadyLoadedException if the plugin is already loaded
  726. * @throws InvalidPluginDescriptorException if the plugin is invalid
  727. */
  728. protected PluginWrapper loadPluginFromPath(Path pluginPath) {
  729. // Test for plugin path duplication
  730. String pluginId = idForPath(pluginPath);
  731. if (pluginId != null) {
  732. throw new PluginAlreadyLoadedException(pluginId, pluginPath);
  733. }
  734. // Retrieve and validate the plugin descriptor
  735. PluginDescriptorFinder pluginDescriptorFinder = getPluginDescriptorFinder();
  736. log.debug("Use '{}' to find plugins descriptors", pluginDescriptorFinder);
  737. log.debug("Finding plugin descriptor for plugin '{}'", pluginPath);
  738. PluginDescriptor pluginDescriptor = pluginDescriptorFinder.find(pluginPath);
  739. validatePluginDescriptor(pluginDescriptor);
  740. // Check there are no loaded plugins with the retrieved id
  741. pluginId = pluginDescriptor.getPluginId();
  742. if (plugins.containsKey(pluginId)) {
  743. PluginWrapper loadedPlugin = getPlugin(pluginId);
  744. throw new PluginRuntimeException("There is an already loaded plugin ({}) "
  745. + "with the same id ({}) as the plugin at path '{}'. Simultaneous loading "
  746. + "of plugins with the same PluginId is not currently supported.\n"
  747. + "As a workaround you may include PluginVersion and PluginProvider "
  748. + "in PluginId.",
  749. loadedPlugin, pluginId, pluginPath);
  750. }
  751. log.debug("Found descriptor {}", pluginDescriptor);
  752. String pluginClassName = pluginDescriptor.getPluginClass();
  753. log.debug("Class '{}' for plugin '{}'", pluginClassName, pluginPath);
  754. // load plugin
  755. log.debug("Loading plugin '{}'", pluginPath);
  756. ClassLoader pluginClassLoader = getPluginLoader().loadPlugin(pluginPath, pluginDescriptor);
  757. log.debug("Loaded plugin '{}' with class loader '{}'", pluginPath, pluginClassLoader);
  758. PluginWrapper pluginWrapper = createPluginWrapper(pluginDescriptor, pluginPath, pluginClassLoader);
  759. // test for disabled plugin
  760. if (isPluginDisabled(pluginDescriptor.getPluginId())) {
  761. log.info("Plugin '{}' is disabled", pluginPath);
  762. pluginWrapper.setPluginState(PluginState.DISABLED);
  763. }
  764. // validate the plugin
  765. if (!isPluginValid(pluginWrapper)) {
  766. log.warn("Plugin '{}' is invalid and it will be disabled", pluginPath);
  767. pluginWrapper.setPluginState(PluginState.DISABLED);
  768. }
  769. log.debug("Created wrapper '{}' for plugin '{}'", pluginWrapper, pluginPath);
  770. pluginId = pluginDescriptor.getPluginId();
  771. // add plugin to the list with plugins
  772. plugins.put(pluginId, pluginWrapper);
  773. getUnresolvedPlugins().add(pluginWrapper);
  774. // add plugin class loader to the list with class loaders
  775. getPluginClassLoaders().put(pluginId, pluginClassLoader);
  776. return pluginWrapper;
  777. }
  778. /**
  779. * Creates the plugin wrapper.
  780. * <p>
  781. * Override this if you want to prevent plugins having full access to the plugin manager.
  782. *
  783. * @param pluginDescriptor the plugin descriptor
  784. * @param pluginPath the path to the plugin
  785. * @param pluginClassLoader the class loader for the plugin
  786. * @return the plugin wrapper
  787. */
  788. protected PluginWrapper createPluginWrapper(PluginDescriptor pluginDescriptor, Path pluginPath, ClassLoader pluginClassLoader) {
  789. // create the plugin wrapper
  790. log.debug("Creating wrapper for plugin '{}'", pluginPath);
  791. PluginWrapper pluginWrapper = new PluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader);
  792. pluginWrapper.setPluginFactory(getPluginFactory());
  793. return pluginWrapper;
  794. }
  795. /**
  796. * Tests for already loaded plugins on given path.
  797. *
  798. * @param pluginPath the path to investigate
  799. * @return id of plugin or null if not loaded
  800. */
  801. protected String idForPath(Path pluginPath) {
  802. for (PluginWrapper plugin : plugins.values()) {
  803. if (plugin.getPluginPath().equals(pluginPath)) {
  804. return plugin.getPluginId();
  805. }
  806. }
  807. return null;
  808. }
  809. /**
  810. * Override this to change the validation criteria.
  811. *
  812. * @param descriptor the plugin descriptor to validate
  813. * @throws InvalidPluginDescriptorException if validation fails
  814. */
  815. protected void validatePluginDescriptor(PluginDescriptor descriptor) {
  816. if (StringUtils.isNullOrEmpty(descriptor.getPluginId())) {
  817. throw new InvalidPluginDescriptorException("Field 'id' cannot be empty");
  818. }
  819. if (descriptor.getVersion() == null) {
  820. throw new InvalidPluginDescriptorException("Field 'version' cannot be empty");
  821. }
  822. }
  823. /**
  824. * Check if the exact version in requires is allowed.
  825. *
  826. * @return true if exact versions in requires is allowed
  827. */
  828. public boolean isExactVersionAllowed() {
  829. return exactVersionAllowed;
  830. }
  831. /**
  832. * Set to true to allow requires expression to be exactly x.y.z.
  833. * The default is false, meaning that using an exact version x.y.z will
  834. * implicitly mean the same as &gt;=x.y.z
  835. *
  836. * @param exactVersionAllowed set to true or false
  837. */
  838. public void setExactVersionAllowed(boolean exactVersionAllowed) {
  839. this.exactVersionAllowed = exactVersionAllowed;
  840. }
  841. @Override
  842. public VersionManager getVersionManager() {
  843. return versionManager;
  844. }
  845. /**
  846. * The plugin label is used in logging, and it's a string in format {@code pluginId@pluginVersion}.
  847. *
  848. * @param pluginDescriptor the plugin descriptor
  849. * @return the plugin label
  850. */
  851. protected String getPluginLabel(PluginDescriptor pluginDescriptor) {
  852. return pluginDescriptor.getPluginId() + "@" + pluginDescriptor.getVersion();
  853. }
  854. @SuppressWarnings("unchecked")
  855. protected <T> List<Class<? extends T>> getExtensionClasses(List<ExtensionWrapper<T>> extensionsWrapper) {
  856. List<Class<? extends T>> extensionClasses = new ArrayList<>(extensionsWrapper.size());
  857. for (ExtensionWrapper<T> extensionWrapper : extensionsWrapper) {
  858. Class<T> c = (Class<T>) extensionWrapper.getDescriptor().extensionClass;
  859. extensionClasses.add(c);
  860. }
  861. return extensionClasses;
  862. }
  863. protected <T> List<T> getExtensions(List<ExtensionWrapper<T>> extensionsWrapper) {
  864. List<T> extensions = new ArrayList<>(extensionsWrapper.size());
  865. for (ExtensionWrapper<T> extensionWrapper : extensionsWrapper) {
  866. try {
  867. extensions.add(extensionWrapper.getExtension());
  868. } catch (PluginRuntimeException e) {
  869. log.error("Cannot retrieve extension", e);
  870. }
  871. }
  872. return extensions;
  873. }
  874. }