001/* 002 * Copyright 2015 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.runtime.plugin; 017 018import java.io.BufferedReader; 019import java.io.ByteArrayInputStream; 020import java.io.File; 021import java.io.FileFilter; 022import java.io.FileInputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.InputStreamReader; 026import java.net.URL; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.Collections; 030import java.util.Enumeration; 031import java.util.HashMap; 032import java.util.Map; 033import java.util.Set; 034 035import javax.xml.XMLConstants; 036import javax.xml.parsers.SAXParserFactory; 037import javax.xml.transform.stream.StreamSource; 038import javax.xml.validation.Schema; 039import javax.xml.validation.SchemaFactory; 040 041import org.apache.avalon.framework.component.ComponentManager; 042import org.apache.avalon.framework.configuration.Configuration; 043import org.apache.avalon.framework.configuration.ConfigurationException; 044import org.apache.avalon.framework.configuration.DefaultConfiguration; 045import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 046import org.apache.avalon.framework.context.Context; 047import org.apache.avalon.framework.context.ContextException; 048import org.apache.avalon.framework.service.WrapperServiceManager; 049import org.apache.commons.io.IOUtils; 050import org.apache.commons.lang3.StringUtils; 051import org.slf4j.Logger; 052import org.slf4j.LoggerFactory; 053import org.xml.sax.XMLReader; 054 055import org.ametys.runtime.config.ConfigDisableConditionsEvaluator; 056import org.ametys.runtime.config.ConfigManager; 057import org.ametys.runtime.model.type.ModelItemTypeExtensionPoint; 058import org.ametys.runtime.plugin.FeatureActivator.PluginsInformation; 059import org.ametys.runtime.plugin.IncludePolicyFeatureActivator.IncludedFeature; 060import org.ametys.runtime.plugin.PluginIssue.PluginIssueCode; 061import org.ametys.runtime.plugin.component.PluginsComponentManager; 062import org.ametys.runtime.servlet.RuntimeConfig; 063 064/** 065 * The PluginManager is in charge to load and initialize plugins. <br> 066 * It gives access to the extension points. 067 */ 068public final class PluginsManager 069{ 070 /** The regexp to determine if a plugin name is ignored (CVS or .* or *.bak or *.old)*/ 071 public static final String PLUGIN_NAMES_IGNORED = "^CVS|\\..+|.*\\.bak|.*\\.old$"; 072 073 /** The regexp to determine if a plugin name is correct (add ^ and $ as delimiters if this is your only test) */ 074 public static final String PLUGIN_NAME_REGEXP = "[a-zA-Z0-9](?:[a-zA-Z0-9-_\\.]*[a-zA-Z0-9])?"; 075 076 /** Separator between pluginName and featureName */ 077 public static final String FEATURE_ID_SEPARATOR = "/"; 078 079 /** Plugin filename */ 080 public static final String PLUGIN_FILENAME = "plugin.xml"; 081 082 /** 083 * Context attribute containing the features to include. 084 * <br>If present {@link PluginsManager} will init with {@link IncludePolicyFeatureActivator} and the provided collection of features (as strings). 085 * <br>If not present, {@link PluginsManager} will init with {@link ExcludePolicyFeatureActivator} which is the default one. 086 * <br>Use with care. 087 */ 088 public static final String INCLUDED_FEATURES_CONTEXT_ATTRIBUTE = IncludePolicyFeatureActivator.IncludedFeature.class.getName(); 089 090 // Map runtimVersion -> XML schema 091 private static final Map<String, String> __PLUGIN_SCHEMAS = Map.of("4.0", "plugin-4.0.xsd", 092 "4.8", "plugin-4.8.xsd"); 093 094 // shared instance 095 private static PluginsManager __instance; 096 097 // safe mode flag 098 private boolean _safeMode; 099 100 // associations plugins/resourcesURI 101 private Map<String, String> _resourceURIs; 102 103 // plugins/locations association 104 private Map<String, File> _locations; 105 106 // All readable plugins 107 private Map<String, Plugin> _allPlugins; 108 109 // Active plugins 110 private Map<String, Plugin> _plugins; 111 112 // Plugin versions 113 private Map<String, String> _versions; 114 115 // Loaded features 116 private Map<String, Feature> _features; 117 118 // All inactive features 119 private Map<String, InactivityStatus> _inactiveFeatures; 120 121 // All declared extension points 122 private Map<String, ExtensionPointDefinition> _extensionPoints; 123 124 // Declared components, stored by role 125 private Map<String, ComponentDefinition> _components; 126 127 // Declared extension, grouped by extension point 128 private Map<String, Map<String, ExtensionDefinition>> _extensions; 129 130 // status after initialization 131 private Status _status; 132 133 // Logger for traces 134 private Logger _logger = LoggerFactory.getLogger(PluginsManager.class); 135 136 // Errors collected during system initialization 137 private Collection<PluginIssue> _errors = new ArrayList<>(); 138 private FeatureActivator _featureActivator; 139 140 private PluginsManager() 141 { 142 // empty constructor 143 } 144 145 /** 146 * Returns the shared instance of the <code>PluginManager</code> 147 * @return the shared instance of the PluginManager 148 */ 149 public static PluginsManager getInstance() 150 { 151 if (__instance == null) 152 { 153 __instance = new PluginsManager(); 154 } 155 156 return __instance; 157 } 158 159 /** 160 * Returns true if the safe mode is activated. 161 * @return true if the safe mode is activated. 162 */ 163 public boolean isSafeMode() 164 { 165 return _safeMode; 166 } 167 168 /** 169 * Returns errors gathered during plugins loading. 170 * @return errors gathered during plugins loading. 171 */ 172 public Collection<PluginIssue> getErrors() 173 { 174 return _errors; 175 } 176 177 /** 178 * Returns the names of the plugins 179 * @return the names of the plugins 180 */ 181 public Set<String> getPluginNames() 182 { 183 return Collections.unmodifiableSet(_plugins.keySet()); 184 } 185 186 /** 187 * Returns a String array containing the names of the plugins bundled in jars 188 * @return a String array containing the names of the plugins bundled in jars 189 */ 190 public Set<String> getBundledPluginsNames() 191 { 192 return Collections.unmodifiableSet(_resourceURIs.keySet()); 193 } 194 195 /** 196 * Returns active plugins declarations. 197 * @return active plugins declarations. 198 */ 199 public Map<String, Plugin> getPlugins() 200 { 201 return Collections.unmodifiableMap(_plugins); 202 } 203 204 /** 205 * Returns all existing plugins definitions. 206 * @return all existing plugins definitions. 207 */ 208 public Map<String, Plugin> getAllPlugins() 209 { 210 return Collections.unmodifiableMap(_allPlugins); 211 } 212 213 /** 214 * Returns loaded features declarations. <br>They may be different than active feature in case of safe mode. 215 * @return loaded features declarations. 216 */ 217 public Map<String, Feature> getFeatures() 218 { 219 return Collections.unmodifiableMap(_features); 220 } 221 222 /** 223 * Returns inactive features id and cause of deactivation. 224 * @return inactive features id and cause of deactivation. 225 */ 226 public Map<String, InactivityStatus> getInactiveFeatures() 227 { 228 return Collections.unmodifiableMap(_inactiveFeatures); 229 } 230 231 /** 232 * Returns the extensions points and their extensions 233 * @return the extensions points and their extensions 234 */ 235 public Map<String, Collection<String>> getExtensionPoints() 236 { 237 Map<String, Collection<String>> result = new HashMap<>(); 238 239 for (String point : _extensions.keySet()) 240 { 241 result.put(point, _extensions.get(point).keySet()); 242 } 243 244 return Collections.unmodifiableMap(result); 245 } 246 247 /** 248 * Returns the components roles. 249 * @return the components roles. 250 */ 251 public Collection<String> getComponents() 252 { 253 return Collections.unmodifiableCollection(_components.keySet()); 254 } 255 256 /** 257 * Returns the base URI for the given plugin resources, or null if the plugin does not exist or is located in the file system. 258 * @param pluginName the name of the plugin 259 * @return the base URI for the given plugin resources, or null if the plugin does not exist or is located in the file system. 260 */ 261 public String getResourceURI(String pluginName) 262 { 263 String pluginUri = _resourceURIs.get(pluginName); 264 if (pluginUri == null || !_plugins.containsKey(pluginName)) 265 { 266 return null; 267 } 268 269 return "resource:/" + pluginUri; 270 } 271 272 /** 273 * Returns the plugin filesystem location for the given plugin or null if the plugin is loaded from the classpath. 274 * @param pluginName the plugin name 275 * @return the plugin location for the given plugin 276 */ 277 public File getPluginLocation(String pluginName) 278 { 279 return _locations.get(pluginName); 280 } 281 282 /** 283 * Get the plugin version or null if unknown. Only available for bundled plugins. 284 * @param pluginName the plugin name 285 * @return the plugin version if known. 286 */ 287 public String getVersion(String pluginName) 288 { 289 return _versions.get(pluginName); 290 } 291 292 /** 293 * Returns the status after initialization. 294 * @return the status after initialization. 295 */ 296 public Status getStatus() 297 { 298 return _status; 299 } 300 301 @SuppressWarnings("unchecked") 302 private void _setActivator(Context context) 303 { 304 Collection<IncludedFeature> includedFeatures = null; 305 try 306 { 307 includedFeatures = (Collection<IncludedFeature>) context.get(INCLUDED_FEATURES_CONTEXT_ATTRIBUTE); 308 } 309 catch (ContextException e) 310 { 311 // object not found 312 } 313 314 if (includedFeatures != null) 315 { 316 _featureActivator = new IncludePolicyFeatureActivator(_allPlugins, includedFeatures); 317 } 318 else 319 { 320 Collection<String> excludedPlugins = RuntimeConfig.getInstance().getExcludedPlugins(); 321 Collection<String> excludedFeatures = RuntimeConfig.getInstance().getExcludedFeatures(); 322 _featureActivator = new ExcludePolicyFeatureActivator(_allPlugins, excludedPlugins, excludedFeatures); 323 } 324 _logger.debug("Using FeatureActivator '{}'", _featureActivator.getClass().getSimpleName()); 325 } 326 327 /** 328 * Initialization of the plugin manager 329 * @param parentCM the parent {@link ComponentManager}. 330 * @param context the Avalon context 331 * @param contextPath the Web context path on the server filesystem 332 * @param forceSafeMode true to force the application to enter the safe mode 333 * @return the {@link PluginsComponentManager} containing loaded components. 334 * @throws Exception if something wrong occurs during plugins loading 335 */ 336 public PluginsComponentManager init(ComponentManager parentCM, Context context, String contextPath, boolean forceSafeMode) throws Exception 337 { 338 _resourceURIs = new HashMap<>(); 339 _locations = new HashMap<>(); 340 _errors = new ArrayList<>(); 341 _versions = new HashMap<>(); 342 343 _safeMode = false; 344 345 // Bundled plugins locations 346 _initResourceURIs(); 347 348 // Additional plugins 349 Map<String, File> externalPlugins = RuntimeConfig.getInstance().getExternalPlugins(); 350 351 // Check external plugins 352 for (File plugin : externalPlugins.values()) 353 { 354 if (!plugin.exists() || !plugin.isDirectory()) 355 { 356 throw new RuntimeException("The configured external plugin is not an existing directory: " + plugin.getAbsolutePath()); 357 } 358 } 359 360 // Plugins root directories (directories containing plugins directories) 361 Collection<String> locations = RuntimeConfig.getInstance().getPluginsLocations(); 362 363 // List of chosen components 364 Map<String, String> componentsConfig = RuntimeConfig.getInstance().getComponents(); 365 366 // List of manually excluded plugins 367 Collection<String> excludedPlugins = RuntimeConfig.getInstance().getExcludedPlugins(); 368 369 // Parse all plugin.xml 370 _allPlugins = _parsePlugins(contextPath, locations, externalPlugins, excludedPlugins); 371 372 if (RuntimeConfig.getInstance().isInvalid()) 373 { 374 _status = Status.RUNTIME_NOT_LOADED; 375 PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath); 376 return safeManager; 377 } 378 379 // Get active feature list 380 _setActivator(context); 381 PluginsInformation info = _featureActivator.computeActiveFeatures(componentsConfig, _safeMode); 382 383 Map<String, Plugin> plugins = info.getPlugins(); 384 Map<String, Feature> features = info.getFeatures(); 385 _errors.addAll(info.getErrors()); 386 387 // At this point, extension points, active and inactive features are known 388 if (_logger.isInfoEnabled()) 389 { 390 String shortDump = _featureActivator.shortDump(info); 391 if (!shortDump.isEmpty()) 392 { 393 _logger.info("\n" + shortDump); 394 } 395 } 396 if (_logger.isDebugEnabled()) 397 { 398 _logger.debug("All declared plugins : \n\n" + _featureActivator.fullDump(info)); 399 } 400 401 if (!_errors.isEmpty()) 402 { 403 _status = Status.WRONG_DEFINITIONS; 404 PluginsComponentManager manager = _enterSafeMode(parentCM, context, contextPath); 405 return manager; 406 } 407 408 // Create the ComponentManager for config 409 PluginsComponentManager configCM = new PluginsComponentManager(parentCM); 410 configCM.setLogger(LoggerFactory.getLogger("org.ametys.runtime.plugin.manager")); 411 configCM.contextualize(context); 412 413 // Create the ComponentManager 414 PluginsComponentManager manager = new PluginsComponentManager(configCM); 415 manager.setLogger(LoggerFactory.getLogger("org.ametys.runtime.plugin.manager")); 416 manager.contextualize(context); 417 418 _initializeConfigurationComponentManager(contextPath, info, configCM); 419 420 // Config loading 421 ConfigManager configManager = ConfigManager.getInstance(); 422 423 configManager.contextualize(context); 424 configManager.service(new WrapperServiceManager(configCM)); 425 configManager.initialize(); 426 427 _parseConfiguration(configManager, plugins, features); 428 429 // force safe mode if requested 430 if (forceSafeMode) 431 { 432 _status = Status.SAFE_MODE_FORCED; 433 PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath); 434 return safeManager; 435 } 436 437 // config file does not exist 438 if (configManager.isEmpty()) 439 { 440 _status = Status.NO_CONFIG; 441 PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath); 442 return safeManager; 443 } 444 445 if (!configManager.isComplete()) 446 { 447 _status = Status.CONFIG_INCOMPLETE; 448 PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath); 449 return safeManager; 450 } 451 452 // Components and single extension point loading 453 Collection<PluginIssue> errors = new ArrayList<>(); 454 _loadExtensionsPoints(manager, info.getExtensionPoints(), info.getExtensions(), contextPath, errors); 455 _loadComponents(manager, info.getComponents(), contextPath, errors); 456 _loadRuntimeInit(manager, errors); 457 458 _errors.addAll(errors); 459 460 if (!errors.isEmpty()) 461 { 462 _status = Status.NOT_INITIALIZED; 463 PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath); 464 return safeManager; 465 } 466 467 _plugins = plugins; 468 _features = features; 469 _inactiveFeatures = info.getInactiveFeatures(); 470 _extensionPoints = info.getExtensionPoints(); 471 _extensions = info.getExtensions(); 472 _components = info.getComponents(); 473 474 try 475 { 476 manager.initialize(); 477 } 478 catch (Exception e) 479 { 480 _logger.error("Caught an exception loading components.", e); 481 482 _status = Status.NOT_INITIALIZED; 483 484 _errors.add(new PluginIssue(null, null, PluginIssueCode.INITIALIZATION_EXCEPTION, null, e.getMessage(), e)); 485 486 // Dispose the first ComponentManager 487 manager.dispose(); 488 manager = null; 489 490 // Then enter safe mode with another ComponentManager 491 PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath); 492 return safeManager; 493 } 494 495 _status = Status.OK; 496 497 return manager; 498 } 499 500 private void _initializeConfigurationComponentManager(String contextPath, PluginsInformation pluginsInfo, PluginsComponentManager configCM) 501 { 502 Collection<PluginIssue> errorsOnConfigTypeEPLoading = new ArrayList<>(); 503 _loadExtensionsPoint(configCM, ModelItemTypeExtensionPoint.ROLE_CONFIG, pluginsInfo.getExtensionPoints(), pluginsInfo.getExtensions(), contextPath, errorsOnConfigTypeEPLoading); 504 _loadComponent(configCM, ConfigDisableConditionsEvaluator.ROLE_FOR_CONFIG_EVALUATOR, pluginsInfo.getComponents(), contextPath, errorsOnConfigTypeEPLoading); 505 506 _errors.addAll(errorsOnConfigTypeEPLoading); 507 if (!errorsOnConfigTypeEPLoading.isEmpty()) 508 { 509 throw new PluginException("Errors while loading extension points needed for configuration validation.", _errors, null); 510 } 511 512 try 513 { 514 configCM.initialize(); 515 } 516 catch (Exception e) 517 { 518 throw new PluginException("Caught exception while starting ComponentManager for configuration validation.", e, _errors, null); 519 } 520 } 521 522 private void _parseConfiguration(ConfigManager configManager, Map<String, Plugin> plugins, Map<String, Feature> features) 523 { 524 // Plugin (global) config parameter loading 525 for (String pluginName : plugins.keySet()) 526 { 527 Plugin plugin = plugins.get(pluginName); 528 configManager.addPluginConfig(pluginName, plugin.getConfigParameters(), plugin.getParameterCheckers()); 529 } 530 531 // Feature (local) config parameter loading 532 for (String featureId : features.keySet()) 533 { 534 Feature feature = features.get(featureId); 535 configManager.addFeatureConfig(feature.getFeatureId(), feature.getConfigParameters(), feature.getParameterCheckers(), feature.getConfigParametersReferences()); 536 } 537 538 // Parse the parameters and check if the config is complete and valid 539 configManager.parseAndValidate(); 540 } 541 542 // Look for plugins bundled in jars 543 // They have a META-INF/ametys-plugins plain text file containing plugin name and path to plugin.xml 544 private void _initResourceURIs() throws IOException 545 { 546 Enumeration<URL> pluginResources = getClass().getClassLoader().getResources("META-INF/ametys-plugins"); 547 548 while (pluginResources.hasMoreElements()) 549 { 550 URL pluginResource = pluginResources.nextElement(); 551 552 try (InputStream is = pluginResource.openStream(); 553 BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"))) 554 { 555 String plugin; 556 while ((plugin = br.readLine()) != null) 557 { 558 String pluginNameAndVersion = StringUtils.substringBefore(plugin, ':'); 559 String pluginName = StringUtils.substringBefore(pluginNameAndVersion, '@'); 560 String version = StringUtils.trimToNull(StringUtils.substringAfter(pluginNameAndVersion, '@')); 561 String pluginResourceURI = StringUtils.substringAfter(plugin, ':'); 562 563 _resourceURIs.put(pluginName, pluginResourceURI); 564 _versions.put(pluginName, version); 565 } 566 } 567 } 568 } 569 570 private Map<String, Plugin> _parsePlugins(String contextPath, Collection<String> locations, Map<String, File> externalPlugins, Collection<String> excludedPlugins) throws IOException 571 { 572 Map<String, Plugin> plugins = new HashMap<>(); 573 574 // Bundled plugins configurations loading 575 for (String pluginName : _resourceURIs.keySet()) 576 { 577 String resourceURI = _resourceURIs.get(pluginName) + "/" + PLUGIN_FILENAME; 578 579 if (getClass().getResource(resourceURI) == null) 580 { 581 _pluginError(pluginName, "A plugin '" + pluginName + "' is declared in a jar, but no file '" + PLUGIN_FILENAME + "' can be found at '" + resourceURI + "'.", PluginIssueCode.BUNDLED_PLUGIN_NOT_PRESENT, excludedPlugins, null); 582 } 583 else if (!pluginName.matches("^" + PLUGIN_NAME_REGEXP + "$")) 584 { 585 _pluginError(pluginName, pluginName + " is an incorrect plugin name.", PluginIssueCode.PLUGIN_NAME_INVALID, excludedPlugins, null); 586 } 587 else if (plugins.containsKey(pluginName)) 588 { 589 _pluginError(pluginName, "The plugin " + pluginName + " at " + resourceURI + " is already declared.", PluginIssueCode.PLUGIN_NAME_EXIST, excludedPlugins, null); 590 } 591 592 _logger.debug("Reading plugin configuration at {}", resourceURI); 593 594 Configuration configuration = null; 595 String path = "resource:/" + resourceURI; 596 try (InputStream is = getClass().getResourceAsStream(resourceURI)) 597 { 598 configuration = _getConfigurationFromStream(pluginName, is, path, excludedPlugins); 599 600 if (configuration != null) 601 { 602 Plugin plugin = new Plugin(pluginName); 603 plugin.configure(configuration); 604 plugins.put(pluginName, plugin); 605 606 _logger.info("Plugin '{}' found at path 'resource:/{}'", pluginName, resourceURI); 607 } 608 } 609 catch (ConfigurationException e) 610 { 611 _pluginError(pluginName, "Unable to configure plugin '" + pluginName + "' at " + path, PluginIssueCode.CONFIGURATION_UNREADABLE, excludedPlugins, e); 612 } 613 } 614 615 // Other plugins configuration loading 616 for (String location : locations) 617 { 618 File locationBase = new File(contextPath, location); 619 620 if (locationBase.exists() && locationBase.isDirectory()) 621 { 622 File[] pluginDirs = locationBase.listFiles(new FileFilter() 623 { 624 public boolean accept(File pathname) 625 { 626 return pathname.isDirectory(); 627 } 628 }); 629 630 for (File pluginDir : pluginDirs) 631 { 632 _addPlugin(plugins, pluginDir.getName(), pluginDir, excludedPlugins); 633 } 634 } 635 } 636 637 // external plugins 638 for (String externalPlugin : externalPlugins.keySet()) 639 { 640 File pluginDir = externalPlugins.get(externalPlugin); 641 642 if (pluginDir.exists() && pluginDir.isDirectory()) 643 { 644 _addPlugin(plugins, externalPlugin, pluginDir, excludedPlugins); 645 } 646 } 647 648 return plugins; 649 } 650 651 private void _addPlugin(Map<String, Plugin> plugins, String pluginName, File pluginDir, Collection<String> excludedPlugins) throws IOException 652 { 653 if (pluginName.matches(PLUGIN_NAMES_IGNORED)) 654 { 655 _logger.debug("Skipping directory {} ...", pluginDir.getAbsolutePath()); 656 return; 657 } 658 659 if (!pluginName.matches("^" + PLUGIN_NAME_REGEXP + "$")) 660 { 661 _logger.warn("{} is an incorrect plugin directory name. It will be ignored.", pluginName); 662 return; 663 } 664 665 File pluginFile = new File(pluginDir, PLUGIN_FILENAME); 666 if (!pluginFile.exists()) 667 { 668 _logger.warn("There is no file named {} in the directory {}. It will be ignored.", PLUGIN_FILENAME, pluginDir.getAbsolutePath()); 669 return; 670 } 671 672 if (plugins.containsKey(pluginName)) 673 { 674 _pluginError(pluginName, "The plugin " + pluginName + " at " + pluginFile.getAbsolutePath() + " is already declared.", PluginIssueCode.PLUGIN_NAME_EXIST, excludedPlugins, null); 675 return; 676 } 677 678 _logger.debug("Reading plugin configuration at {}", pluginFile.getAbsolutePath()); 679 680 Configuration configuration = null; 681 String path = pluginFile.getAbsolutePath(); 682 try (InputStream is = new FileInputStream(pluginFile)) 683 { 684 configuration = _getConfigurationFromStream(pluginName, is, path, excludedPlugins); 685 686 if (configuration != null) 687 { 688 Plugin plugin = new Plugin(pluginName); 689 plugin.configure(configuration); 690 plugins.put(pluginName, plugin); 691 692 _locations.put(pluginName, pluginDir); 693 _logger.info("Plugin '{}' found at path '{}'", pluginName, pluginFile.getAbsolutePath()); 694 } 695 } 696 catch (ConfigurationException e) 697 { 698 _pluginError(pluginName, "Unable to configure plugin '" + pluginName + "' at " + path, PluginIssueCode.CONFIGURATION_UNREADABLE, excludedPlugins, e); 699 } 700 } 701 702 private Configuration _getConfigurationFromStream(String pluginName, InputStream is, String path, Collection<String> excludedPlugins) 703 { 704 try 705 { 706 byte[] pluginConf = IOUtils.toByteArray(is); 707 708 SAXParserFactory factory = SAXParserFactory.newInstance(); 709 factory.setNamespaceAware(true); 710 XMLReader reader = factory.newSAXParser().getXMLReader(); 711 DefaultConfigurationBuilder confBuilder = new DefaultConfigurationBuilder(reader); 712 713 Configuration configuration = confBuilder.build(new ByteArrayInputStream(pluginConf), path); 714 715 String runtimeVersion = configuration.getAttribute("runtimeVersion", null); 716 String schemaFile = __PLUGIN_SCHEMAS.get(runtimeVersion); 717 718 if (schemaFile == null) 719 { 720 throw new IllegalArgumentException("Unknown runtimeVersion " + runtimeVersion + " for plugin at path " + path); 721 } 722 723 SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 724 URL schemaURL = getClass().getResource(schemaFile); 725 Schema schema = schemaFactory.newSchema(schemaURL); 726 schema.newValidator().validate(new StreamSource(new ByteArrayInputStream(pluginConf))); 727 728 return configuration; 729 } 730 catch (Exception e) 731 { 732 _pluginError(pluginName, "Unable to access to plugin '" + pluginName + "' at " + path, PluginIssueCode.CONFIGURATION_UNREADABLE, excludedPlugins, e); 733 return null; 734 } 735 } 736 737 private void _pluginError(String pluginName, String message, PluginIssueCode code, Collection<String> excludedPlugins, Exception e) 738 { 739 // ignore errors for manually excluded plugins 740 if (!excludedPlugins.contains(pluginName)) 741 { 742 PluginIssue issue = new PluginIssue(null, null, code, null, message, e); 743 _errors.add(issue); 744 _logger.error(message, e); 745 } 746 } 747 748 private void _loadExtensionsPoint(PluginsComponentManager manager, String point, Map<String, ExtensionPointDefinition> extensionPoints, Map<String, Map<String, ExtensionDefinition>> extensionsDefinitions, String contextPath, Collection<PluginIssue> errors) 749 { 750 ExtensionPointDefinition definition = extensionPoints.get(point); 751 Configuration conf = definition._configuration; 752 String clazz = conf.getAttribute("class", null); 753 String pluginName = definition._pluginName; 754 755 try 756 { 757 Class<? extends Object> c = Class.forName(clazz); 758 759 // check that the class is actually an ExtensionPoint 760 if (ExtensionPoint.class.isAssignableFrom(c)) 761 { 762 Class<? extends ExtensionPoint> extensionClass = c.asSubclass(ExtensionPoint.class); 763 764 // Load extensions 765 Collection<ExtensionDefinition> extensionDefinitions = new ArrayList<>(); 766 Map<String, ExtensionDefinition> initialDefinitions = extensionsDefinitions.get(point); 767 768 if (initialDefinitions != null) 769 { 770 for (String id : initialDefinitions.keySet()) 771 { 772 ExtensionDefinition extensionDefinition = initialDefinitions.get(id); 773 Configuration initialConf = extensionDefinition.getConfiguration(); 774 Configuration realExtensionConf = _getComponentConfiguration(initialConf, contextPath, extensionDefinition.getPluginName(), errors); 775 extensionDefinitions.add(new ExtensionDefinition(id, point, extensionDefinition.getPluginName(), extensionDefinition.getFeatureName(), realExtensionConf)); 776 } 777 } 778 779 Configuration realComponentConf = _getComponentConfiguration(conf, contextPath, pluginName, errors); 780 manager.addExtensionPoint(pluginName, point, extensionClass, realComponentConf, extensionDefinitions); 781 } 782 else 783 { 784 String message = "In plugin '" + pluginName + "', the extension point '" + point + "' references class '" + clazz + "' which don't implement " + ExtensionPoint.class.getName(); 785 _logger.error(message); 786 PluginIssue issue = new PluginIssue(pluginName, null, PluginIssue.PluginIssueCode.EXTENSIONPOINT_CLASS_INVALID, conf.getLocation(), message); 787 errors.add(issue); 788 } 789 } 790 catch (ClassNotFoundException e) 791 { 792 String message = "In plugin '" + pluginName + "', the extension point '" + point + "' references the unexisting class '" + clazz + "'."; 793 _logger.error(message, e); 794 PluginIssue issue = new PluginIssue(pluginName, null, PluginIssue.PluginIssueCode.CLASSNOTFOUND, conf.getLocation(), message); 795 errors.add(issue); 796 } 797 } 798 799 private void _loadExtensionsPoints(PluginsComponentManager manager, Map<String, ExtensionPointDefinition> extensionPoints, Map<String, Map<String, ExtensionDefinition>> extensionsDefinitions, String contextPath, Collection<PluginIssue> errors) 800 { 801 extensionPoints.keySet() 802 .stream() 803 .filter(point -> !ModelItemTypeExtensionPoint.ROLE_CONFIG.equals(point)) // Point already loaded to parse config parameters 804 .forEach(point -> _loadExtensionsPoint(manager, point, extensionPoints, extensionsDefinitions, contextPath, errors)); 805 } 806 807 @SuppressWarnings("unchecked") 808 private void _loadComponent(PluginsComponentManager manager, String role, Map<String, ComponentDefinition> components, String contextPath, Collection<PluginIssue> errors) 809 { 810 ComponentDefinition componentDefinition = components.get(role); 811 Configuration componentConf = componentDefinition.getConfiguration(); 812 Configuration realComponentConf = _getComponentConfiguration(componentConf, contextPath, componentDefinition.getPluginName(), errors); 813 814 // XML schema ensures class is not null 815 String clazz = componentConf.getAttribute("class", null); 816 assert clazz != null; 817 818 try 819 { 820 Class c = Class.forName(clazz); 821 manager.addComponent(componentDefinition.getPluginName(), componentDefinition.getFeatureName(), role, c, realComponentConf); 822 } 823 catch (ClassNotFoundException ex) 824 { 825 String message = "In feature '" + componentDefinition.getPluginName() + FEATURE_ID_SEPARATOR + componentDefinition.getFeatureName() + "', the component '" + role + "' references the unexisting class '" + clazz + "'."; 826 _logger.error(message, ex); 827 PluginIssue issue = new PluginIssue(componentDefinition.getPluginName(), componentDefinition.getFeatureName(), PluginIssueCode.CLASSNOTFOUND, componentConf.getLocation(), message); 828 errors.add(issue); 829 } 830 } 831 832 private void _loadComponents(PluginsComponentManager manager, Map<String, ComponentDefinition> components, String contextPath, Collection<PluginIssue> errors) 833 { 834 components.keySet() 835 .stream() 836 .filter(role -> !ConfigDisableConditionsEvaluator.ROLE_FOR_CONFIG_EVALUATOR.equals(role)) // Component already loaded to parse config parameters 837 .forEach(role -> _loadComponent(manager, role, components, contextPath, errors)); 838 } 839 840 private Configuration _getComponentConfiguration(Configuration initialConfiguration, String contextPath, String pluginName, Collection<PluginIssue> errors) 841 { 842 String config = initialConfiguration.getAttribute("config", null); 843 844 if (config != null) 845 { 846 String configPath = null; 847 848 try 849 { 850 // If the config attribute is present, it is either a plugin-relative, or a webapp-relative path (starting with '/') 851 if (config.startsWith("/")) 852 { 853 // absolute path 854 File configFile = new File(contextPath, config); 855 configPath = configFile.getAbsolutePath(); 856 857 if (!configFile.exists() || configFile.isDirectory()) 858 { 859 if (_logger.isInfoEnabled()) 860 { 861 _logger.info("No config file was found at " + configPath + ". Using internally declared config."); 862 } 863 864 return initialConfiguration; 865 } 866 867 try (InputStream is = new FileInputStream(configFile)) 868 { 869 return new DefaultConfigurationBuilder(true).build(is, configPath); 870 } 871 } 872 else 873 { 874 // relative path 875 String baseUri = _resourceURIs.get(pluginName); 876 if (baseUri == null) 877 { 878 File pluginLocation = getPluginLocation(pluginName); 879 880 File configFile = new File(pluginLocation, config); 881 configPath = configFile.getAbsolutePath(); 882 883 if (!configFile.exists() || configFile.isDirectory()) 884 { 885 if (_logger.isInfoEnabled()) 886 { 887 _logger.info("No config file was found at " + configPath + ". Using internally declared config."); 888 } 889 890 return initialConfiguration; 891 } 892 893 try (InputStream is = new FileInputStream(configFile)) 894 { 895 return new DefaultConfigurationBuilder(true).build(is, configPath); 896 } 897 } 898 else 899 { 900 String path = baseUri + "/" + config; 901 configPath = "resource:/" + path; 902 903 try (InputStream is = getClass().getResourceAsStream(path)) 904 { 905 if (is == null) 906 { 907 if (_logger.isInfoEnabled()) 908 { 909 _logger.info("No config file was found at " + configPath + ". Using internally declared config."); 910 } 911 912 return initialConfiguration; 913 } 914 915 return new DefaultConfigurationBuilder(true).build(is, configPath); 916 } 917 } 918 } 919 } 920 catch (Exception ex) 921 { 922 String message = "Unable to load external configuration defined in the plugin " + pluginName; 923 _logger.error(message, ex); 924 PluginIssue issue = new PluginIssue(pluginName, null, PluginIssueCode.EXTERNAL_CONFIGURATION, initialConfiguration.getLocation(), message); 925 errors.add(issue); 926 } 927 } 928 929 return initialConfiguration; 930 } 931 932 private void _loadRuntimeInit(PluginsComponentManager manager, Collection<PluginIssue> errors) 933 { 934 String className = RuntimeConfig.getInstance().getInitClassName(); 935 936 if (className != null) 937 { 938 _logger.info("Loading init class '{}' for application", className); 939 940 try 941 { 942 Class<?> initClass = Class.forName(className); 943 if (!Init.class.isAssignableFrom(initClass)) 944 { 945 String message = "Provided init class " + initClass + " does not implement " + Init.class.getName(); 946 _logger.error(message); 947 PluginIssue issue = new PluginIssue(null, null, PluginIssue.PluginIssueCode.INIT_CLASS_INVALID, null, message); 948 errors.add(issue); 949 return; 950 } 951 952 manager.addComponent(null, null, Init.ROLE, initClass, new DefaultConfiguration("component")); 953 _logger.info("Init class {} loaded", className); 954 } 955 catch (ClassNotFoundException e) 956 { 957 String message = "The application init class '" + className + "' does not exist."; 958 _logger.error(message, e); 959 PluginIssue issue = new PluginIssue(null, null, PluginIssueCode.CLASSNOTFOUND, null, message); 960 errors.add(issue); 961 } 962 963 } 964 else if (_logger.isInfoEnabled()) 965 { 966 _logger.info("No init class configured"); 967 } 968 } 969 970 private PluginsComponentManager _enterSafeMode(ComponentManager parentCM, Context context, String contextPath) throws Exception 971 { 972 _logger.info("Entering safe mode due to previous errors ..."); 973 _safeMode = true; 974 975 ExcludePolicyFeatureActivator safeModeFeatureActivator = new ExcludePolicyFeatureActivator(_allPlugins, Collections.EMPTY_LIST, Collections.EMPTY_LIST); 976 PluginsInformation info = safeModeFeatureActivator.computeActiveFeatures(Collections.EMPTY_MAP, _safeMode); 977 978 _plugins = info.getPlugins(); 979 _extensionPoints = info.getExtensionPoints(); 980 _components = info.getComponents(); 981 _extensions = info.getExtensions(); 982 _features = info.getFeatures(); 983 _inactiveFeatures = info.getInactiveFeatures(); 984 985 if (_logger.isDebugEnabled()) 986 { 987 _logger.debug("Safe mode : \n\n" + safeModeFeatureActivator.fullDump(info)); 988 } 989 990 Collection<PluginIssue> errors = info.getErrors(); 991 if (!errors.isEmpty()) 992 { 993 // errors while in safe mode ... 994 throw new PluginException("Errors while loading components in safe mode.", _errors, errors); 995 } 996 997 // Create the ComponentManager for config 998 PluginsComponentManager configCM = new PluginsComponentManager(parentCM); 999 configCM.setLogger(LoggerFactory.getLogger("org.ametys.runtime.plugin.manager")); 1000 configCM.contextualize(context); 1001 1002 // Create the ComponentManager 1003 PluginsComponentManager manager = new PluginsComponentManager(configCM); 1004 manager.setLogger(LoggerFactory.getLogger("org.ametys.runtime.plugin.manager")); 1005 manager.contextualize(context); 1006 1007 _loadExtensionsPoint(configCM, ModelItemTypeExtensionPoint.ROLE_CONFIG, info.getExtensionPoints(), info.getExtensions(), contextPath, errors); 1008 _loadComponent(configCM, ConfigDisableConditionsEvaluator.ROLE_FOR_CONFIG_EVALUATOR, info.getComponents(), contextPath, errors); 1009 configCM.initialize(); 1010 1011 ConfigManager.getInstance().service(new WrapperServiceManager(configCM)); 1012 1013 errors = new ArrayList<>(); 1014 _loadExtensionsPoints(manager, _extensionPoints, _extensions, contextPath, errors); 1015 _loadComponents(manager, _components, contextPath, errors); 1016 1017 if (!errors.isEmpty()) 1018 { 1019 // errors while in safe mode ... 1020 throw new PluginException("Errors while loading components in safe mode.", _errors, errors); 1021 } 1022 1023 try 1024 { 1025 manager.initialize(); 1026 } 1027 catch (Exception e) 1028 { 1029 throw new PluginException("Caught exception while starting ComponentManager in safe mode.", e, _errors, null); 1030 } 1031 1032 return manager; 1033 } 1034 1035 /** 1036 * Cause of the deactivation of a feature 1037 */ 1038 public enum InactivityCause 1039 { 1040 /** Constant for excluded features */ 1041 EXCLUDED, 1042 /** Constant for features deactivated by other features */ 1043 DEACTIVATED, 1044 /** Constant for features overridden by another feature */ 1045 OVERRIDDEN, 1046 /**Constant for features disabled due to not chosen component */ 1047 COMPONENT, 1048 /** Constant for features disabled due to missing dependencies */ 1049 DEPENDENCY, 1050 /** Constant for passive features that are not necessary (nobody depends on it) */ 1051 PASSIVE, 1052 /** Constant for features disabled to wrong referenced extension point */ 1053 INVALID_POINT, 1054 /** Feature is not safe while in safe mode */ 1055 NOT_SAFE, 1056 /** Constant for features not enabled by {@link IncludePolicyFeatureActivator} (the feature is not needed as no enabled feature depends on it) */ 1057 UNUSED 1058 } 1059 1060 /** 1061 * PluginsManager status after initialization. 1062 */ 1063 public enum Status 1064 { 1065 /** Everything is ok. All features were correctly loaded */ 1066 OK, 1067 /** There was no errors, but the configuration is missing */ 1068 NO_CONFIG, 1069 /** There was no errors, but the configuration is incomplete */ 1070 CONFIG_INCOMPLETE, 1071 /** Something was wrong when reading plugins definitions */ 1072 WRONG_DEFINITIONS, 1073 /** There were issues during components loading */ 1074 NOT_INITIALIZED, 1075 /** The runtime.xml could not be loaded */ 1076 RUNTIME_NOT_LOADED, 1077 /** Safe mode has been forced */ 1078 SAFE_MODE_FORCED 1079 } 1080 1081 /** 1082 * Represents the cause for an inactive feature, along with features causing the deactivation (overrides, ...) 1083 * @param cause the inactivity cause 1084 * @param features the features causing the inactivity, or null if none 1085 * 1086 */ 1087 public record InactivityStatus(InactivityCause cause, Collection<String> features) { /* empty */ } 1088}