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