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.model.type.ModelItemTypeExtensionPoint;
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().isInvalid())
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, ModelItemTypeExtensionPoint.ROLE_CONFIG, 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 (!ModelItemTypeExtensionPoint.ROLE_CONFIG.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, ModelItemTypeExtensionPoint.ROLE_CONFIG, 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        /** Constant for excluded features */
992        EXCLUDED,
993        /** Constant for features deactivated by other features */
994        DEACTIVATED,
995        /** Constant for features overridden by another feature */
996        OVERRIDDEN,        
997        /**Constant for features disabled due to not chosen component */
998        COMPONENT,
999        /** Constant for features disabled due to missing dependencies */
1000        DEPENDENCY,
1001        /** Constant for passive features that are not necessary (nobody depends on it) */
1002        PASSIVE, 
1003        /** Constant for features disabled to wrong referenced extension point */
1004        INVALID_POINT, 
1005        /** Feature is not safe while in safe mode */
1006        NOT_SAFE,        
1007        /** Constant for features not enabled by {@link IncludePolicyFeatureActivator} (the feature is not needed as no enabled feature depends on it) */
1008        UNUSED
1009    }
1010    
1011    /**
1012     * PluginsManager status after initialization.
1013     */
1014    public enum Status
1015    {
1016        /** Everything is ok. All features were correctly loaded */
1017        OK,
1018        /** There was no errors, but the configuration is missing */
1019        NO_CONFIG,
1020        /** There was no errors, but the configuration is incomplete */
1021        CONFIG_INCOMPLETE,
1022        /** Something was wrong when reading plugins definitions */
1023        WRONG_DEFINITIONS,
1024        /** There were issues during components loading */
1025        NOT_INITIALIZED, 
1026        /** The runtime.xml could not be loaded */
1027        RUNTIME_NOT_LOADED,
1028        /** Safe mode has been forced */
1029        SAFE_MODE_FORCED
1030    }
1031}