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