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