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}