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