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.Iterator;
032import java.util.LinkedHashMap;
033import java.util.Map;
034import java.util.Map.Entry;
035import java.util.Set;
036
037import javax.xml.XMLConstants;
038import javax.xml.parsers.SAXParserFactory;
039import javax.xml.validation.Schema;
040import javax.xml.validation.SchemaFactory;
041
042import org.apache.avalon.framework.component.ComponentManager;
043import org.apache.avalon.framework.configuration.Configuration;
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.service.WrapperServiceManager;
048import org.apache.commons.collections.CollectionUtils;
049import org.apache.commons.io.IOUtils;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052import org.xml.sax.XMLReader;
053
054import org.ametys.runtime.config.ConfigManager;
055import org.ametys.runtime.plugin.PluginIssue.PluginIssueCode;
056import org.ametys.runtime.plugin.component.PluginsComponentManager;
057import org.ametys.runtime.servlet.RuntimeConfig;
058
059/**
060 * The PluginManager is in charge to load and initialize plugins. <br>
061 * It gives access to the extension points.
062 */
063public final class PluginsManager
064{
065    /** The regexp to determine if a plugin name is ignored (CVS or .* or *.bak or *.old)*/
066    public static final String PLUGIN_NAMES_IGNORED = "^CVS|\\..+|.*\\.bak|.*\\.old$";
067    
068    /** The regexp to determine if a plugin name is correct (add ^ and $ as delimiters if this is your only test) */
069    public static final String PLUGIN_NAME_REGEXP = "[a-zA-Z0-9](?:[a-zA-Z0-9-_\\.]*[a-zA-Z0-9])?";
070    
071    /** Separator between pluginName and featureName */
072    public static final String FEATURE_ID_SEPARATOR = "/";
073
074    /** Plugin filename */
075    public static final String PLUGIN_FILENAME = "plugin.xml";
076
077    // shared instance
078    private static PluginsManager __instance;
079    
080    // safe mode flag
081    private boolean _safeMode;
082    
083    // associations plugins/resourcesURI
084    private Map<String, String> _resourceURIs;
085    
086    // plugins/locations association
087    private Map<String, File> _locations;
088    
089    // All readable plugins
090    private Map<String, Plugin> _allPlugins;
091
092    // Active plugins
093    private Map<String, Plugin> _plugins;
094
095    // Loaded features
096    private Map<String, Feature> _features;
097
098    // All declared features
099    private Map<String, InactivityCause> _inactiveFeatures;
100
101    // All declared extension points
102    private Map<String, ExtensionPointDefinition> _extensionPoints;
103    
104    // Declared components, stored by role
105    private Map<String, ComponentDefinition> _components;
106    
107    // Declared extension, grouped by extension point
108    private Map<String, Map<String, ExtensionDefinition>> _extensions;
109    
110    // status after initialization
111    private Status _status;
112    
113    // Logger for traces
114    private Logger _logger = LoggerFactory.getLogger(PluginsManager.class);
115
116    // Errors collected during system initialization
117    private Collection<PluginIssue> _errors = new ArrayList<>();
118
119    private PluginsManager()
120    {
121        // empty constructor
122    }
123    
124    /**
125     * Returns the shared instance of the <code>PluginManager</code>
126     * @return the shared instance of the PluginManager
127     */
128    public static PluginsManager getInstance()
129    {
130        if (__instance == null)
131        {
132            __instance = new PluginsManager();
133        }
134
135        return __instance;
136    }
137    
138    /**
139     * Returns true if the safe mode is activated. 
140     * @return true if the safe mode is activated.
141     */
142    public boolean isSafeMode()
143    {
144        return _safeMode;
145    }
146    
147    /**
148     * Returns errors gathered during plugins loading.
149     * @return errors gathered during plugins loading.
150     */
151    public Collection<PluginIssue> getErrors()
152    {
153        return _errors;
154    }
155
156    /**
157     * Returns the names of the plugins
158     * @return the names of the plugins
159     */
160    public Set<String> getPluginNames()
161    {
162        return Collections.unmodifiableSet(_plugins.keySet());
163    }
164    
165    /**
166     * Returns a String array containing the names of the plugins bundled in jars
167     * @return a String array containing the names of the plugins bundled in jars
168     */
169    public Set<String> getBundledPluginsNames()
170    {
171        return Collections.unmodifiableSet(_resourceURIs.keySet());
172    }
173
174    /**
175     * Returns active plugins declarations.
176     * @return active plugins declarations.
177     */
178    public Map<String, Plugin> getPlugins()
179    {
180        return Collections.unmodifiableMap(_plugins);
181    }
182
183    /**
184     * Returns all existing plugins definitions.
185     * @return all existing plugins definitions.
186     */
187    public Map<String, Plugin> getAllPlugins()
188    {
189        return Collections.unmodifiableMap(_allPlugins);
190    }
191    
192    /**
193     * Returns loaded features declarations. <br>They may be different than active feature in case of safe mode.
194     * @return loaded features declarations.
195     */
196    public Map<String, Feature> getFeatures()
197    {
198        return Collections.unmodifiableMap(_features);
199    }
200    
201    /**
202     * Returns inactive features id and cause of deactivation.
203     * @return inactive features id and cause of deactivation.
204     */
205    public Map<String, InactivityCause> getInactiveFeatures()
206    {
207        return Collections.unmodifiableMap(_inactiveFeatures);
208    }
209
210    /**
211     * Returns the extensions points and their extensions
212     * @return the extensions points and their extensions
213     */
214    public Map<String, Collection<String>> getExtensionPoints()
215    {
216        Map<String, Collection<String>> result = new HashMap<>();
217        
218        for (String point : _extensions.keySet())
219        {
220            result.put(point, _extensions.get(point).keySet());
221        }
222        
223        return Collections.unmodifiableMap(result);
224    }
225    
226    /**
227     * Returns the components roles.
228     * @return the components roles.
229     */
230    public Collection<String> getComponents()
231    {
232        return Collections.unmodifiableCollection(_components.keySet());
233    }
234    
235    /**
236     * Returns the base URI for the given plugin resources, or null if the plugin does not exist or is located in the file system.
237     * @param pluginName the name of the plugin
238     * @return the base URI for the given plugin resources, or null if the plugin does not exist or is located in the file system.
239     */
240    public String getResourceURI(String pluginName)
241    {
242        String pluginUri = _resourceURIs.get(pluginName);
243        if (pluginUri == null || !_plugins.containsKey(pluginName))
244        {
245            return null;
246        }
247        
248        return "resource:/" + pluginUri; 
249    }
250    
251    /**
252     * Returns the plugin filesystem location for the given plugin or null if the plugin is loaded from the classpath.
253     * @param pluginName the plugin name
254     * @return the plugin location for the given plugin
255     */
256    public File getPluginLocation(String pluginName)
257    {
258        return _locations.get(pluginName);
259    }
260    
261    /**
262     * Returns the status after initialization.
263     * @return the status after initialization.
264     */
265    public Status getStatus()
266    {
267        return _status;
268    }
269    
270    /**
271     * Initialization of the plugin manager
272     * @param parentCM the parent {@link ComponentManager}.
273     * @param context the Avalon context
274     * @param contextPath the Web context path on the server filesystem
275     * @param forceSafeMode true to force the application to enter the safe mode
276     * @return the {@link PluginsComponentManager} containing loaded components.
277     * @throws Exception if something wrong occurs during plugins loading
278     */
279    public PluginsComponentManager init(ComponentManager parentCM, Context context, String contextPath, boolean forceSafeMode) throws Exception
280    {
281        _resourceURIs = new HashMap<>();
282        _locations = new HashMap<>();
283        _errors = new ArrayList<>();
284        
285        _safeMode = false;
286
287        // Bundled plugins locations
288        _initResourceURIs();
289        
290        // Additional plugins 
291        Map<String, File> externalPlugins = RuntimeConfig.getInstance().getExternalPlugins();
292        
293        // Check external plugins
294        for (File plugin : externalPlugins.values())
295        {
296            if (!plugin.exists() || !plugin.isDirectory())
297            {
298                throw new RuntimeException("The configured external plugin is not an existing directory: " + plugin.getAbsolutePath());
299            }
300        }
301        
302        // Plugins root directories (directories containing plugins directories)
303        Collection<String> locations = RuntimeConfig.getInstance().getPluginsLocations();
304        
305        // List of chosen components
306        Map<String, String> componentsConfig = RuntimeConfig.getInstance().getComponents();
307        
308        // List of manually excluded plugins
309        Collection<String> excludedPlugins = RuntimeConfig.getInstance().getExcludedPlugins();
310        
311        // List of manually excluded features
312        Collection<String> excludedFeatures = RuntimeConfig.getInstance().getExcludedFeatures();
313        
314        // Parse all plugin.xml
315        _allPlugins = _parsePlugins(contextPath, locations, externalPlugins, excludedPlugins);
316
317        if (RuntimeConfig.getInstance().isSafeMode())
318        {
319            _status = Status.RUNTIME_NOT_LOADED;
320            PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath);
321            return safeManager;
322        }
323        
324        // Get active feature list
325        PluginsInformation info = computeActiveFeatures(contextPath, excludedPlugins, excludedFeatures, componentsConfig);
326        
327        Map<String, Plugin> plugins = info.getPlugins();
328        Map<String, Feature> features = info.getFeatures();
329        _errors.addAll(info.getErrors());
330        
331        // At this point, extension points, active and inactive features are known
332        if (_logger.isDebugEnabled())
333        {
334            _logger.debug("All declared plugins : \n\n" + dump(info.getInactiveFeatures()));
335        }
336        
337        if (!_errors.isEmpty())
338        {
339            _status = Status.WRONG_DEFINITIONS;
340            PluginsComponentManager manager = _enterSafeMode(parentCM, context, contextPath);
341            return manager;
342        }
343        
344        // Create the ComponentManager
345        PluginsComponentManager manager = new PluginsComponentManager(parentCM);
346        manager.setLogger(LoggerFactory.getLogger("org.ametys.runtime.plugin.manager"));
347        manager.contextualize(context);
348        
349        // Config loading
350        ConfigManager configManager = ConfigManager.getInstance();
351        
352        configManager.contextualize(context);
353        configManager.service(new WrapperServiceManager(manager));
354        configManager.initialize();
355        
356        // Global config parameter loading
357        for (String pluginName : plugins.keySet())
358        {
359            Plugin plugin = plugins.get(pluginName);
360            configManager.addGlobalConfig(pluginName, plugin.getConfigParameters(), plugin.getParameterCheckers());
361        }
362        
363        // "local" config parameter loading
364        for (String featureId : features.keySet())
365        {
366            Feature feature = features.get(featureId);
367            configManager.addConfig(feature.getFeatureId(), feature.getConfigParameters(), feature.getConfigParametersReferences(), feature.getParameterCheckers());
368        }
369        
370        // check if the config is complete and valid
371        configManager.validate();
372        
373        // force safe mode if requested
374        if (forceSafeMode)
375        {
376            _status = Status.SAFE_MODE_FORCED;
377            PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath);
378            return safeManager;
379        }
380        
381        // config file does not exist
382        if (configManager.isEmpty())
383        {
384            _status = Status.NO_CONFIG;
385            PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath);
386            return safeManager;
387        }
388        
389        if (!configManager.isComplete())
390        {
391            _status = Status.CONFIG_INCOMPLETE;
392            PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath);
393            return safeManager;
394        }
395        
396        // Components and single extension point loading
397        Collection<PluginIssue> errors = new ArrayList<>();
398        _loadExtensionsPoints(manager, info.getExtensionPoints(), info.getExtensions(), contextPath, errors);
399        _loadComponents(manager, info.getComponents(), contextPath, errors);
400        _loadRuntimeInit(manager, errors);
401        
402        _errors.addAll(errors);
403        
404        if (!errors.isEmpty())
405        {
406            _status = Status.NOT_INITIALIZED;
407            PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath);
408            return safeManager;
409        }
410        
411        _plugins = plugins;
412        _features = features;
413        _inactiveFeatures = info.getInactiveFeatures();
414        _extensionPoints = info.getExtensionPoints();
415        _extensions = info.getExtensions();
416        _components = info.getComponents();
417        
418        try
419        {
420            manager.initialize();
421        }
422        catch (Exception e)
423        {
424            _logger.error("Caught an exception loading components.", e);
425            
426            _status = Status.NOT_INITIALIZED;
427
428            // Dispose the first ComponentManager
429            manager.dispose();
430            manager = null;
431            
432            // Then enter safe mode with another ComponentManager
433            PluginsComponentManager safeManager = _enterSafeMode(parentCM, context, contextPath);
434            return safeManager;
435        }
436        
437        _status = Status.OK;
438        
439        return manager;
440    }
441    
442    /**
443     * Outputs the structure of the plugins.
444     * @param inactiveFeatures id and cause of inactive features
445     * @return a String representation of all existing plugins and features.
446     */
447    public String dump(Map<String, InactivityCause> inactiveFeatures)
448    {
449        Collection<String> excludedPlugins = RuntimeConfig.getInstance().getExcludedPlugins();
450        StringBuilder sb = new StringBuilder();
451        
452        for (String pluginName : _allPlugins.keySet())
453        {
454            Plugin plugin = _allPlugins.get(pluginName);
455            sb.append(_dumpPlugin(plugin, excludedPlugins, inactiveFeatures));
456        }
457        
458        if (!_errors.isEmpty())
459        {
460            sb.append("\nErrors :\n");
461            _errors.forEach(issue -> sb.append(issue.toString()).append('\n'));
462        }
463        
464        return sb.toString();
465    }
466    
467    private String _dumpPlugin(Plugin plugin, Collection<String> excludedPlugins, Map<String, InactivityCause> inactiveFeatures)
468    {
469        StringBuilder sb = new StringBuilder();
470        
471        String pluginName = plugin.getName();
472        sb.append("Plugin ").append(pluginName);
473        
474        if (excludedPlugins.contains(pluginName))
475        {
476            sb.append("   *** excluded ***");
477        }
478        
479        sb.append('\n');
480        
481        Collection<String> configParameters = plugin.getConfigParameters().keySet();
482        if (!CollectionUtils.isEmpty(configParameters))
483        {
484            sb.append("  Config parameters : \n");
485            configParameters.forEach(param -> sb.append("    ").append(param).append('\n'));
486        }
487        
488        Collection<String> paramCheckers = plugin.getParameterCheckers().keySet();
489        if (!CollectionUtils.isEmpty(paramCheckers))
490        {
491            sb.append("  Parameters checkers : \n");
492            paramCheckers.forEach(param -> sb.append("    ").append(param).append('\n'));
493        }
494        
495        Collection<String> extensionPoints = plugin.getExtensionPoints();
496        if (!CollectionUtils.isEmpty(extensionPoints))
497        {
498            sb.append("  Extension points : \n");
499            extensionPoints.forEach(point -> sb.append("    ").append(point).append('\n'));
500        }
501        
502        Map<String, Feature> features = plugin.getFeatures();
503        for (String featureId : features.keySet())
504        {
505            Feature feature = features.get(featureId);
506            sb.append(_dumpFeature(feature, inactiveFeatures));
507        }
508        
509        sb.append('\n');
510        
511        return sb.toString();
512    }
513    
514    private String _dumpFeature(Feature feature, Map<String, InactivityCause> inactiveFeatures)
515    {
516        StringBuilder sb = new StringBuilder();
517        String featureId = feature.getFeatureId();
518        
519        sb.append("  Feature ").append(featureId);
520        if (feature.isPassive())
521        {
522            sb.append(" (passive)");
523        }
524
525        if (feature.isSafe())
526        {
527            sb.append(" (safe)");
528        }
529        
530        if (inactiveFeatures != null && inactiveFeatures.containsKey(featureId))
531        {
532            sb.append("   *** inactive (").append(inactiveFeatures.get(featureId)).append(") ***");
533        }
534        
535        sb.append('\n');
536        
537        Collection<String> featureConfigParameters = feature.getConfigParameters().keySet();
538        if (!CollectionUtils.isEmpty(featureConfigParameters))
539        {
540            sb.append("    Config parameters : \n");
541            featureConfigParameters.forEach(param -> sb.append("      ").append(param).append('\n'));
542        }
543        
544        Collection<String> configParametersReferences = feature.getConfigParametersReferences();
545        if (!CollectionUtils.isEmpty(configParametersReferences))
546        {
547            sb.append("    Config parameters references : \n");
548            configParametersReferences.forEach(param -> sb.append("      ").append(param).append('\n'));
549        }
550        
551        Collection<String> featureParamCheckers = feature.getParameterCheckers().keySet();
552        if (!CollectionUtils.isEmpty(featureParamCheckers))
553        {
554            sb.append("    Parameters checkers : \n");
555            featureParamCheckers.forEach(param -> sb.append("    ").append(param).append('\n'));
556        }
557        
558        Map<String, String> componentsIds = feature.getComponentsIds();
559        if (!componentsIds.isEmpty())
560        {
561            sb.append("    Components : \n");
562            
563            for (String role : componentsIds.keySet())
564            {
565                String id = componentsIds.get(role);
566                sb.append("      ").append(role).append(" : ").append(id).append('\n');
567            }
568            
569            sb.append('\n');
570        }
571
572        Map<String, Collection<String>> extensionsIds = feature.getExtensionsIds();
573        if (!extensionsIds.isEmpty())
574        {
575            sb.append("    Extensions : \n");
576            
577            for (Entry<String, Collection<String>> extensionEntry : extensionsIds.entrySet())
578            {
579                String point = extensionEntry.getKey();
580                Collection<String> ids = extensionEntry.getValue();
581                
582                sb.append("      ").append(point).append(" :\n");
583                ids.forEach(id -> sb.append("        ").append(id).append('\n'));
584            }
585            
586            sb.append('\n');
587        }
588        
589        return sb.toString();
590    }
591    
592    // Look for plugins bundled in jars
593    // They have a META-INF/ametys-plugins plain text file containing plugin name and path to plugin.xml
594    private void _initResourceURIs() throws IOException
595    {
596        Enumeration<URL> pluginResources = getClass().getClassLoader().getResources("META-INF/ametys-plugins");
597        
598        while (pluginResources.hasMoreElements())
599        {
600            URL pluginResource = pluginResources.nextElement();
601            
602            try (InputStream is = pluginResource.openStream();
603                 BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")))
604            {
605                String plugin;
606                while ((plugin = br.readLine()) != null)
607                {
608                    int i = plugin.indexOf(':');
609                    if (i != -1)
610                    {
611                        String pluginName = plugin.substring(0, i);       
612                        String pluginResourceURI = plugin.substring(i + 1);
613                    
614                        _resourceURIs.put(pluginName, pluginResourceURI);
615                    }
616                }
617            }
618        }
619    }
620    
621    private Map<String, Plugin> _parsePlugins(String contextPath, Collection<String> locations, Map<String, File> externalPlugins, Collection<String> excludedPlugins) throws IOException
622    {
623        Map<String, Plugin> plugins = new HashMap<>();
624        
625        // Bundled plugins configurations loading
626        for (String pluginName : _resourceURIs.keySet())
627        {
628            String resourceURI = _resourceURIs.get(pluginName) + "/" + PLUGIN_FILENAME;
629            
630            if (getClass().getResource(resourceURI) == null)
631            {
632                _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);
633            }
634            else if (!pluginName.matches("^" + PLUGIN_NAME_REGEXP + "$"))
635            {
636                _pluginError(pluginName, pluginName + " is an incorrect plugin name.", PluginIssueCode.PLUGIN_NAME_INVALID, excludedPlugins, null);
637            }
638            else if (plugins.containsKey(pluginName))
639            {
640                _pluginError(pluginName, "The plugin " + pluginName + " at " + resourceURI + " is already declared.", PluginIssueCode.PLUGIN_NAME_EXIST, excludedPlugins, null);
641            }
642
643            _logger.debug("Reading plugin configuration at {}", resourceURI);
644
645            Configuration configuration = null;
646            try (InputStream is = getClass().getResourceAsStream(resourceURI))
647            {
648                configuration = _getConfigurationFromStream(pluginName, is, "resource:/" + resourceURI, excludedPlugins);
649            }
650
651            if (configuration != null)
652            {
653                Plugin plugin = new Plugin(pluginName);
654                plugin.configure(configuration);
655                plugins.put(pluginName, plugin);
656
657                _logger.info("Plugin '{}' found at path 'resource:/{}'", pluginName, resourceURI);
658            }
659        }
660        
661        // Other plugins configuration loading
662        for (String location : locations)
663        {
664            File locationBase = new File(contextPath, location);
665
666            if (locationBase.exists() && locationBase.isDirectory())
667            {
668                File[] pluginDirs = locationBase.listFiles(new FileFilter() 
669                {
670                    public boolean accept(File pathname)
671                    {
672                        return pathname.isDirectory();
673                    }
674                });
675                
676                for (File pluginDir : pluginDirs)
677                {
678                    _addPlugin(plugins, pluginDir.getName(), pluginDir, excludedPlugins);
679                }
680            }
681        }
682        
683        // external plugins
684        for (String externalPlugin : externalPlugins.keySet())
685        {
686            File pluginDir = externalPlugins.get(externalPlugin);
687
688            if (pluginDir.exists() && pluginDir.isDirectory())
689            {
690                _addPlugin(plugins, externalPlugin, pluginDir, excludedPlugins);
691            }
692        }
693        
694        return plugins;
695    }
696    
697    private void _addPlugin(Map<String, Plugin> plugins, String pluginName, File pluginDir, Collection<String> excludedPlugins) throws IOException
698    {
699        if (pluginName.matches(PLUGIN_NAMES_IGNORED))
700        {
701            _logger.debug("Skipping directory {} ...", pluginDir.getAbsolutePath());
702            return;
703        }
704        
705        if (!pluginName.matches("^" + PLUGIN_NAME_REGEXP + "$"))
706        {
707            _logger.warn("{} is an incorrect plugin directory name. It will be ignored.", pluginName);
708            return;
709        }
710        
711        File pluginFile = new File(pluginDir, PLUGIN_FILENAME);
712        if (!pluginFile.exists())
713        {
714            _logger.warn("There is no file named {} in the directory {}. It will be ignored.", PLUGIN_FILENAME, pluginDir.getAbsolutePath());
715            return;
716        }
717
718        if (plugins.containsKey(pluginName))
719        {
720            _pluginError(pluginName, "The plugin " + pluginName + " at " + pluginFile.getAbsolutePath() + " is already declared.", PluginIssueCode.PLUGIN_NAME_EXIST, excludedPlugins, null);
721            return;
722        }
723        
724        _logger.debug("Reading plugin configuration at {}", pluginFile.getAbsolutePath());
725
726        Configuration configuration = null;
727        try (InputStream is = new FileInputStream(pluginFile))
728        {
729            configuration = _getConfigurationFromStream(pluginName, is, pluginFile.getAbsolutePath(), excludedPlugins);
730        }
731
732        if (configuration != null)
733        {
734            Plugin plugin = new Plugin(pluginName);
735            plugin.configure(configuration);
736            plugins.put(pluginName, plugin);
737            
738            _locations.put(pluginName, pluginDir);
739            _logger.info("Plugin '{}' found at path '{}'", pluginName, pluginFile.getAbsolutePath());
740        }
741    }
742
743    private Configuration _getConfigurationFromStream(String pluginName, InputStream is, String path, Collection<String> excludedPlugins)
744    {
745        try
746        {
747            SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
748            URL schemaURL = getClass().getResource("plugin-4.0.xsd");
749            Schema schema = schemaFactory.newSchema(schemaURL);
750            SAXParserFactory factory = SAXParserFactory.newInstance();
751            factory.setNamespaceAware(true);
752            factory.setSchema(schema);
753            XMLReader reader = factory.newSAXParser().getXMLReader();
754            DefaultConfigurationBuilder confBuilder = new DefaultConfigurationBuilder(reader);
755            
756            return confBuilder.build(is, path);
757        }
758        catch (Exception e)
759        {
760            _pluginError(pluginName, "Unable to access to plugin '" + pluginName + "' at " + path, PluginIssueCode.CONFIGURATION_UNREADABLE, excludedPlugins, e);
761            return null;
762        }
763    }
764    
765    private void _pluginError(String pluginName, String message, PluginIssueCode code, Collection<String> excludedPlugins, Exception e)
766    {
767        // ignore errors for manually excluded plugins
768        if (!excludedPlugins.contains(pluginName))
769        {
770            PluginIssue issue = new PluginIssue(null, null, code, null, message, e);
771            _errors.add(issue);
772            _logger.error(message, e);
773        }
774    }
775    
776    /**
777     * Computes the actual plugins and features to load, based on values selected by the administrator.<br>
778     * This method don't actually load nor execute any Java code. It reads plugins definitions, selects active features and get components and extensions definitions.
779     * @param contextPath the application context path.
780     * @param excludedPlugins manually excluded plugins.
781     * @param excludedFeatures manually excluded features.
782     * @param componentsConfig chosen components, among those with the same role.
783     * @return all informations gathered during plugins reading.
784     */
785    private PluginsInformation computeActiveFeatures(String contextPath, Collection<String> excludedPlugins, Collection<String> excludedFeatures, Map<String, String> componentsConfig)
786    {
787        Map<String, Feature> initialFeatures = new HashMap<>();
788        Map<String, ExtensionPointDefinition> extensionPoints = new HashMap<>();
789        Map<String, InactivityCause> inactiveFeatures = new HashMap<>();
790        
791        Collection<PluginIssue> errors = new ArrayList<>();
792
793        // Get actual plugin list, corresponding extension points and initial feature list
794        Map<String, Plugin> plugins = _computeActivePlugins(excludedPlugins, initialFeatures, inactiveFeatures, extensionPoints, errors);
795
796        // Compute incoming deactivations
797        Map<String, Collection<String>> incomingDeactivations = _computeIncomingDeactivations(initialFeatures);
798        
799        // First remove user-excluded features
800        Set<String> ids = initialFeatures.keySet();
801        Iterator<String> it = ids.iterator();
802        while (it.hasNext())
803        {
804            String id = it.next();
805            
806            if (excludedFeatures.contains(id))
807            {
808                _logger.debug("Remove excluded feature '{}'", id);
809                it.remove();
810                inactiveFeatures.put(id, InactivityCause.EXCLUDED);
811            }
812        }
813        
814        // Then remove deactivated features
815        // Also remove feature containing inactive components
816        _removeInactiveFeatures(initialFeatures, inactiveFeatures, incomingDeactivations, componentsConfig);
817        
818        ids = initialFeatures.keySet();
819        it = ids.iterator();
820        while (it.hasNext())
821        {
822            String id = it.next();
823            Feature feature = initialFeatures.get(id);
824            Map<String, Collection<String>> extensionsIds = feature.getExtensionsIds();
825            boolean hasBeenRemoved = false;
826            for (String point : extensionsIds.keySet())
827            {
828                if (!extensionPoints.containsKey(point))
829                {
830                    String message = "In feature '" + id + "' an extension references the non-existing point '" + point + "'.";
831                    _logger.error(message);
832                    PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.INVALID_POINT, null, message);
833                    errors.add(issue);
834                    if (!hasBeenRemoved)
835                    {
836                        it.remove();
837                        inactiveFeatures.put(id, InactivityCause.INVALID_POINT);
838                        hasBeenRemoved = true;
839                    }
840                }
841            }
842        }
843        
844        // Process outgoing dependencies
845        Map<String, Feature> features = _processOutgoingDependencies(initialFeatures, inactiveFeatures, errors);
846
847        // Compute incoming dependencies
848        Map<String, Collection<String>> incomingDependencies = _computeIncompingDependencies(features);
849        
850        // Finally remove unused passive features
851        ids = features.keySet();
852        it = ids.iterator();
853        while (it.hasNext())
854        {
855            String id = it.next();
856            Feature feature = features.get(id);
857            
858            if (feature.isPassive() && !incomingDependencies.containsKey(id))
859            {
860                _logger.debug("Remove passive feature '{}'", id);
861                it.remove();
862                inactiveFeatures.put(id, InactivityCause.PASSIVE);
863            }
864        }
865        
866        // Check uniqueness of extensions and components
867        Map<String, Map<String, ExtensionDefinition>> extensions = _computeExtensions(features, errors);
868        Map<String, ComponentDefinition> components = _computeComponents(features, componentsConfig, errors);
869        
870        return new PluginsInformation(plugins, features, inactiveFeatures, extensionPoints, extensions, components, errors);
871    }
872    
873    private Map<String, Plugin> _computeActivePlugins(Collection<String> excludedPlugins, Map<String, Feature> initialFeatures, Map<String, InactivityCause> inactiveFeatures, Map<String, ExtensionPointDefinition> extensionPoints, Collection<PluginIssue> errors)
874    {
875        Map<String, Plugin> plugins = new HashMap<>();
876        for (String pluginName : _allPlugins.keySet())
877        {
878            if (!excludedPlugins.contains(pluginName))
879            {
880                Plugin plugin = _allPlugins.get(pluginName);
881                plugins.put(pluginName, plugin);
882                _logger.info("Plugin '{}' loaded", pluginName);
883
884                // Check uniqueness of extension points
885                Map<String, ExtensionPointDefinition> extPoints = plugin.getExtensionPointDefinitions();
886                for (String point : extPoints.keySet())
887                {
888                    ExtensionPointDefinition definition = extPoints.get(point);
889                    
890                    if (!_safeMode || definition._safe)
891                    {
892                        if (extensionPoints.containsKey(point))
893                        {
894                            // It is an error to have two extension points with the same id, but we should not interrupt when in safe mode, so just ignore it
895                            String message = "The extension point '" + point + "', defined in the plugin '" + pluginName + "' is already defined in aother plugin. ";
896                            PluginIssue issue = new PluginIssue(pluginName, null, PluginIssue.PluginIssueCode.EXTENSIONPOINT_ALREADY_EXIST, definition._configuration.getLocation(), message);
897    
898                            if (!_safeMode)
899                            {
900                                _logger.error(message);
901                                errors.add(issue);
902                            }
903                            else
904                            {
905                                _logger.debug("[Safe mode] {}", message);
906                            }
907                        }
908                        else
909                        {
910                            extensionPoints.put(point, definition);
911                        }
912                    }
913                }
914                
915                Map<String, Feature> features = plugin.getFeatures();
916                for (String id : features.keySet())
917                {
918                    Feature feature = features.get(id);
919                    
920                    if (!_safeMode || feature.isSafe())
921                    {
922                        initialFeatures.put(id, feature);
923                    }
924                    else
925                    {
926                        inactiveFeatures.put(id, InactivityCause.NOT_SAFE);
927                    }
928                }
929            }
930            else
931            {
932                _logger.debug("Plugin '{}' is excluded", pluginName);
933            }
934        }
935
936        return plugins;
937    }
938    
939    private void _removeInactiveFeatures(Map<String, Feature> initialFeatures, Map<String, InactivityCause> inactiveFeatures, Map<String, Collection<String>> incomingDeactivations, Map<String, String> componentsConfig)
940    {
941        Iterator<String> it = initialFeatures.keySet().iterator();
942        while (it.hasNext())
943        {
944            String id = it.next();
945            Feature feature = initialFeatures.get(id);
946            
947            if (incomingDeactivations.containsKey(id) && !incomingDeactivations.get(id).isEmpty())
948            {
949                String deactivatingFeature = incomingDeactivations.get(id).iterator().next();
950                _logger.debug("Removing feature {} deactivated by feature {}.", id, deactivatingFeature);
951                it.remove();
952                inactiveFeatures.put(id, InactivityCause.DEACTIVATED);
953                continue;
954            }
955            
956            Map<String, String> components = feature.getComponentsIds();
957            for (String role : components.keySet())
958            {
959                String componentId = components.get(role);
960                String selectedId = componentsConfig.get(role);
961                
962                // remove the feature if the user asked for a specific id and the declared component has not that id 
963                if (selectedId != null && !selectedId.equals(componentId))
964                {
965                    _logger.debug("Removing feature '{}' as it contains the component id '{}' for role '{}' but the user selected the id '{}' for that role.", id, componentId, role, selectedId);
966                    it.remove();
967                    inactiveFeatures.put(id, InactivityCause.COMPONENT);
968                    continue;
969                }
970            }
971        }
972    }
973    
974    private Map<String, Collection<String>> _computeIncomingDeactivations(Map<String, Feature> features)
975    {
976        Map<String, Collection<String>> incomingDeactivations = new HashMap<>();
977        
978        for (String id : features.keySet())
979        {
980            Feature feature = features.get(id);
981            Collection<String> deactivations = feature.getDeactivations();
982            
983            for (String deactivation : deactivations)
984            {
985                Collection<String> deps = incomingDeactivations.get(deactivation);
986                if (deps == null)
987                {
988                    deps = new ArrayList<>();
989                    incomingDeactivations.put(deactivation, deps);
990                }
991                
992                deps.add(id);
993            }
994        }
995        
996        return incomingDeactivations;
997    }
998    
999    private Map<String, Feature> _processOutgoingDependencies(Map<String, Feature> initialFeatures, Map<String, InactivityCause> inactiveFeatures, Collection<PluginIssue> errors)
1000    {
1001        // Check outgoing dependencies
1002        boolean processDependencies = true;
1003        while (processDependencies)
1004        {
1005            processDependencies = false;
1006            
1007            Collection<String> ids = initialFeatures.keySet();
1008            Iterator<String> it = ids.iterator();
1009            while (it.hasNext())
1010            {
1011                String id = it.next();
1012                Feature feature = initialFeatures.get(id);
1013                Collection<String> dependencies = feature.getDependencies();
1014                for (String dependency : dependencies)
1015                {
1016                    if (!initialFeatures.containsKey(dependency))
1017                    {
1018                        _logger.debug("The feature '{}' depends on '{}' which is not present. It will be ignored.", id, dependency);
1019                        it.remove();
1020                        inactiveFeatures.put(id, InactivityCause.DEPENDENCY);
1021                        processDependencies = true;
1022                    }
1023                }
1024            }
1025        }
1026        
1027        // Reorder remaining features, respecting dependencies
1028        LinkedHashMap<String, Feature> features = new LinkedHashMap<>();
1029        
1030        for (String featureId : initialFeatures.keySet())
1031        {
1032            _computeFeaturesDependencies(featureId, initialFeatures, features, featureId, errors);
1033        }
1034        
1035        return features;
1036    }
1037    
1038    private Map<String, Collection<String>> _computeIncompingDependencies(Map<String, Feature> features)
1039    {
1040        Map<String, Collection<String>> incomingDependencies = new HashMap<>();
1041        for (String id : features.keySet())
1042        {
1043            Feature feature = features.get(id);
1044            Collection<String> dependencies = feature.getDependencies();
1045            
1046            for (String dependency : dependencies)
1047            {
1048                Collection<String> deps = incomingDependencies.get(dependency);
1049                if (deps == null)
1050                {
1051                    deps = new ArrayList<>();
1052                    incomingDependencies.put(dependency, deps);
1053                }
1054                
1055                deps.add(id);
1056            }
1057        }
1058        
1059        return incomingDependencies;
1060    }
1061
1062    private void _computeFeaturesDependencies(String featureId, Map<String, Feature> features, Map<String, Feature> result, String initialFeatureId, Collection<PluginIssue> errors)
1063    {
1064        Feature feature = features.get(featureId);
1065        Collection<String> dependencies = feature.getDependencies();
1066        
1067        for (String dependency : dependencies)
1068        {
1069            if (initialFeatureId.equals(dependency))
1070            {
1071                String message = "Circular dependency detected for feature: " + feature;
1072                _logger.error(message);
1073                PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.CIRCULAR_DEPENDENCY, null, message);
1074                errors.add(issue);
1075            }
1076            else if (!result.containsKey(dependency))
1077            {
1078                // do not process the feature if it has already been processed
1079                _computeFeaturesDependencies(dependency, features, result, initialFeatureId, errors);
1080            }
1081        }
1082        
1083        result.put(featureId, feature);
1084    }
1085    
1086    private Map<String, Map<String, ExtensionDefinition>> _computeExtensions(Map<String, Feature> features, Collection<PluginIssue> errors)
1087    {
1088        Map<String, Map<String, ExtensionDefinition>> extensionsDefinitions = new HashMap<>();
1089        for (Feature feature : features.values())
1090        {
1091            // extensions
1092            Map<String, Map<String, ExtensionDefinition>> extensionsConfs = feature.getExtensions();
1093            for (String point : extensionsConfs.keySet())
1094            {
1095                Map<String, ExtensionDefinition> featureExtensions = extensionsConfs.get(point);
1096                Map<String, ExtensionDefinition> globalExtensions = extensionsDefinitions.get(point);
1097                if (globalExtensions == null)
1098                {
1099                    globalExtensions = new LinkedHashMap<>(featureExtensions);
1100                    extensionsDefinitions.put(point, globalExtensions);
1101                }
1102                else
1103                {
1104                    for (String id : featureExtensions.keySet())
1105                    {
1106                        if (globalExtensions.containsKey(id))
1107                        {
1108                            String message = "The extension '" + id + "' to point '" + point + "' is already defined in another feature.";
1109                            _logger.error(message);
1110                            PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.EXTENSION_ALREADY_EXIST, null, message);
1111                            errors.add(issue);
1112                        }
1113                        else
1114                        {
1115                            ExtensionDefinition definition = featureExtensions.get(id);
1116                            globalExtensions.put(id, definition);
1117                        }
1118                    }
1119                }
1120            }
1121        }
1122        
1123        return extensionsDefinitions;
1124    }
1125    
1126    private Map<String, ComponentDefinition> _computeComponents(Map<String, Feature> features, Map<String, String> componentsConfig, Collection<PluginIssue> errors)
1127    {
1128        Map<String, ComponentDefinition> components = new HashMap<>();
1129        
1130        for (Feature feature : features.values())
1131        {
1132            // components
1133            Map<String, ComponentDefinition> featureComponents = feature.getComponents();
1134            for (String role : featureComponents.keySet())
1135            {
1136                ComponentDefinition definition = featureComponents.get(role);
1137                ComponentDefinition globalDefinition = components.get(role);
1138                if (globalDefinition == null)
1139                {
1140                    components.put(role, definition);
1141                }
1142                else
1143                {
1144                    String id = definition.getId();
1145                    if (id.equals(globalDefinition.getId()))
1146                    {
1147                        String message = "The component for role '" + role + "' and id '" + id + "' is defined both in feature '" + definition.getPluginName() + FEATURE_ID_SEPARATOR + definition.getFeatureName() + "' and in feature '" + globalDefinition.getPluginName() + FEATURE_ID_SEPARATOR + globalDefinition.getFeatureName() + "'.";
1148                        _logger.error(message);
1149                        PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.COMPONENT_ALREADY_EXIST, null, message);
1150                        errors.add(issue);
1151                    }
1152                    else
1153                    {
1154                        String message = "The component for role '" + role + "' is defined with id '" + id + "' in the feature '" + definition.getPluginName() + FEATURE_ID_SEPARATOR + definition.getFeatureName() + "' and with id '" + globalDefinition.getId() + "' in the feature '" + globalDefinition.getPluginName() + FEATURE_ID_SEPARATOR + globalDefinition.getFeatureName() + "'. One of them should be chosen in the runtime.xml.";
1155                        _logger.error(message);
1156                        PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.COMPONENT_ALREADY_EXIST, null, message);
1157                        errors.add(issue);
1158                    }
1159                }
1160            }
1161        }
1162        
1163        // check that each component choosen in the runtime.xml is actually defined
1164        for (String role : componentsConfig.keySet())
1165        {
1166            String requiredId = componentsConfig.get(role);
1167            ComponentDefinition definition = components.get(role);
1168            
1169            if (definition == null || !definition.getId().equals(requiredId))
1170            {
1171                // Due to preceding checks, the definition id should not be different than requiredId, but two checks are always better than one ...
1172                String message = "The component for role '" + role + "' should point to id '" + requiredId + "' but no component match.";
1173                _logger.error(message);
1174                PluginIssue issue = new PluginIssue(null, null, PluginIssueCode.COMPONENT_NOT_DECLARED, null, message);
1175                errors.add(issue);
1176            }
1177        }
1178        
1179        return components;
1180    }
1181    
1182    private void _loadExtensionsPoints(PluginsComponentManager manager, Map<String, ExtensionPointDefinition> extensionPoints, Map<String, Map<String, ExtensionDefinition>> extensionsDefinitions, String contextPath, Collection<PluginIssue> errors)
1183    {
1184        for (String point : extensionPoints.keySet())
1185        {
1186            ExtensionPointDefinition definition = extensionPoints.get(point);
1187            Configuration conf = definition._configuration;
1188            String clazz = conf.getAttribute("class", null);
1189            String pluginName = definition._pluginName;
1190            
1191            try
1192            {
1193                Class<? extends Object> c = Class.forName(clazz);
1194
1195                // check that the class is actually an ExtensionPoint
1196                if (ExtensionPoint.class.isAssignableFrom(c))
1197                {
1198                    Class<? extends ExtensionPoint> extensionClass = c.asSubclass(ExtensionPoint.class);
1199                    
1200                    // Load extensions
1201                    Collection<ExtensionDefinition> extensionDefinitions = new ArrayList<>();
1202                    Map<String, ExtensionDefinition> initialDefinitions = extensionsDefinitions.get(point);
1203                    
1204                    if (initialDefinitions != null)
1205                    {
1206                        for (String id : initialDefinitions.keySet())
1207                        {
1208                            ExtensionDefinition extensionDefinition = initialDefinitions.get(id);
1209                            Configuration initialConf = extensionDefinition.getConfiguration();
1210                            Configuration realExtensionConf = _getComponentConfiguration(initialConf, contextPath, extensionDefinition.getPluginName(), errors);
1211                            extensionDefinitions.add(new ExtensionDefinition(id, point, extensionDefinition.getPluginName(), extensionDefinition.getFeatureName(), realExtensionConf));
1212                        }
1213                    }
1214                    
1215                    Configuration realComponentConf = _getComponentConfiguration(conf, contextPath, pluginName, errors);
1216                    manager.addExtensionPoint(pluginName, point, extensionClass, realComponentConf, extensionDefinitions);
1217                }
1218                else
1219                {
1220                    String message = "In plugin '" + pluginName + "', the extension point '" + point + "' references class '" + clazz + "' which don't implement " + ExtensionPoint.class.getName();
1221                    _logger.error(message);
1222                    PluginIssue issue = new PluginIssue(pluginName, null, PluginIssue.PluginIssueCode.EXTENSIONPOINT_CLASS_INVALID, conf.getLocation(), message);
1223                    errors.add(issue);
1224                }
1225            }
1226            catch (ClassNotFoundException e)
1227            {
1228                String message = "In plugin '" + pluginName + "', the extension point '" + point + "' references the unexisting class '" + clazz + "'.";
1229                _logger.error(message, e);
1230                PluginIssue issue = new PluginIssue(pluginName, null, PluginIssue.PluginIssueCode.CLASSNOTFOUND, conf.getLocation(), message);
1231                errors.add(issue);
1232            }
1233        }
1234    }
1235    
1236    @SuppressWarnings("unchecked")
1237    private void _loadComponents(PluginsComponentManager manager, Map<String, ComponentDefinition> components, String contextPath, Collection<PluginIssue> errors)
1238    {
1239        for (String role : components.keySet())
1240        {
1241            ComponentDefinition componentDefinition = components.get(role);
1242            Configuration componentConf = componentDefinition.getConfiguration();
1243            Configuration realComponentConf = _getComponentConfiguration(componentConf, contextPath, componentDefinition.getPluginName(), errors);
1244
1245            // XML schema ensures class is not null
1246            String clazz = componentConf.getAttribute("class", null);
1247            assert clazz != null;
1248            
1249            try
1250            {
1251                Class c = Class.forName(clazz);
1252                manager.addComponent(componentDefinition.getPluginName(), componentDefinition.getFeatureName(), role, c, realComponentConf);
1253            }
1254            catch (ClassNotFoundException ex)
1255            {
1256                String message = "In feature '" + componentDefinition.getPluginName() + FEATURE_ID_SEPARATOR + componentDefinition.getFeatureName() + "', the component '" + role + "' references the unexisting class '" + clazz + "'.";
1257                _logger.error(message, ex);
1258                PluginIssue issue = new PluginIssue(componentDefinition.getPluginName(), componentDefinition.getFeatureName(), PluginIssueCode.CLASSNOTFOUND, componentConf.getLocation(), message);
1259                errors.add(issue);
1260            }
1261        }
1262    }
1263    
1264    private Configuration _getComponentConfiguration(Configuration initialConfiguration, String contextPath, String pluginName, Collection<PluginIssue> errors)
1265    {
1266        String config = initialConfiguration.getAttribute("config", null);
1267        
1268        if (config != null)
1269        {
1270            @SuppressWarnings("resource") InputStream is = null;
1271            String configPath = null;
1272            
1273            try
1274            {
1275                // If the config attribute is present, it is either a plugin-relative, or a webapp-relative path (starting with '/')  
1276                if (config.startsWith("/"))
1277                {
1278                    // absolute path
1279                    File configFile = new File(contextPath, config);
1280                    configPath = configFile.getAbsolutePath();
1281                    
1282                    if (!configFile.exists() || configFile.isDirectory())
1283                    {
1284                        if (_logger.isInfoEnabled())
1285                        {
1286                            _logger.info("No config file was found at " + configPath + ". Using internally declared config.");
1287                        }
1288                        
1289                        return initialConfiguration;
1290                    }
1291                    
1292                    is = new FileInputStream(configFile);
1293                }
1294                else
1295                {
1296                    // relative path
1297                    String baseUri = _resourceURIs.get(pluginName);
1298                    if (baseUri == null)
1299                    {
1300                        File pluginLocation = getPluginLocation(pluginName);
1301                        
1302                        File configFile = new File(pluginLocation, config);
1303                        configPath = configFile.getAbsolutePath();
1304
1305                        if (!configFile.exists() || configFile.isDirectory())
1306                        {
1307                            if (_logger.isInfoEnabled())
1308                            {
1309                                _logger.info("No config file was found at " + configPath + ". Using internally declared config.");
1310                            }
1311                            
1312                            return initialConfiguration;
1313                        }
1314
1315                        is = new FileInputStream(configFile);
1316                    }
1317                    else
1318                    {
1319                        String path = baseUri + "/" + config;
1320                        configPath = "resource:/" + path;
1321                        is = getClass().getResourceAsStream(path);
1322                        
1323                        if (is == null)
1324                        {
1325                            if (_logger.isInfoEnabled())
1326                            {
1327                                _logger.info("No config file was found at " + configPath + ". Using internally declared config.");
1328                            }
1329                            
1330                            return initialConfiguration;
1331                        }
1332                    }
1333                }
1334                
1335                return new DefaultConfigurationBuilder(true).build(is, configPath);
1336            }
1337            catch (Exception ex)
1338            {
1339                String message = "Unable to load external configuration defined in the plugin " + pluginName;
1340                _logger.error(message, ex);
1341                PluginIssue issue = new PluginIssue(pluginName, null, PluginIssueCode.EXTERNAL_CONFIGURATION, initialConfiguration.getLocation(), message);
1342                errors.add(issue);
1343            }
1344            finally
1345            {
1346                IOUtils.closeQuietly(is);
1347            }
1348        }
1349        
1350        return initialConfiguration;
1351    }
1352
1353    private void _loadRuntimeInit(PluginsComponentManager manager, Collection<PluginIssue> errors)
1354    {
1355        String className = RuntimeConfig.getInstance().getInitClassName();
1356
1357        if (className != null)
1358        {
1359            _logger.info("Loading init class '{}' for application", className);
1360            
1361            try
1362            {
1363                Class<?> initClass = Class.forName(className);
1364                if (!Init.class.isAssignableFrom(initClass))
1365                {
1366                    String message = "Provided init class " + initClass + " does not implement " + Init.class.getName();
1367                    _logger.error(message);
1368                    PluginIssue issue = new PluginIssue(null, null, PluginIssue.PluginIssueCode.INIT_CLASS_INVALID, null, message);
1369                    errors.add(issue);
1370                    return;
1371                }
1372                
1373                manager.addComponent(null, null, Init.ROLE, initClass, new DefaultConfiguration("component"));
1374                _logger.info("Init class {} loaded", className);
1375            }
1376            catch (ClassNotFoundException e)
1377            {
1378                String message = "The application init class '" + className + "' does not exist.";
1379                _logger.error(message, e);
1380                PluginIssue issue = new PluginIssue(null, null, PluginIssueCode.CLASSNOTFOUND, null, message);
1381                errors.add(issue);
1382            }
1383            
1384        }
1385        else if (_logger.isInfoEnabled())
1386        {
1387            _logger.info("No init class configured");
1388        }
1389    }
1390    
1391    private PluginsComponentManager _enterSafeMode(ComponentManager parentCM, Context context, String contextPath)
1392    {
1393        _logger.info("Entering safe mode due to previous errors ...");
1394        _safeMode = true;
1395        
1396        PluginsInformation info = computeActiveFeatures(contextPath, Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_MAP);
1397        
1398        _plugins = info.getPlugins();
1399        _extensionPoints = info.getExtensionPoints();
1400        _components = info.getComponents();
1401        _extensions = info.getExtensions();
1402        _features = info.getFeatures();
1403        _inactiveFeatures = info.getInactiveFeatures();
1404        
1405        if (_logger.isDebugEnabled())
1406        {
1407            _logger.debug("Safe mode : \n\n" + dump(_inactiveFeatures));
1408        }
1409        
1410        Collection<PluginIssue> errors = info.getErrors();
1411        if (!errors.isEmpty())
1412        {
1413            // errors while in safe mode ... 
1414            throw new PluginException("Errors while loading components in safe mode.", _errors, errors);
1415        }
1416
1417        // Create the ComponentManager
1418        PluginsComponentManager manager = new PluginsComponentManager(parentCM);
1419        manager.setLogger(LoggerFactory.getLogger("org.ametys.runtime.plugin.manager"));
1420        manager.contextualize(context);
1421        
1422        ConfigManager.getInstance().service(new WrapperServiceManager(manager));
1423
1424        errors = new ArrayList<>();
1425        _loadExtensionsPoints(manager, _extensionPoints, _extensions, contextPath, errors);
1426        _loadComponents(manager, _components, contextPath, errors);
1427        
1428        if (!errors.isEmpty())
1429        {
1430            // errors while in safe mode ... 
1431            throw new PluginException("Errors while loading components in safe mode.", _errors, errors);
1432        }
1433        
1434        try
1435        {
1436            manager.initialize();
1437        }
1438        catch (Exception e)
1439        {
1440            throw new PluginException("Caught exception while starting ComponentManager in safe mode.", e, _errors, null);
1441        }
1442        
1443        return manager;
1444    }
1445    
1446    /**
1447     * Cause of the deactivation of a feature
1448     */
1449    public enum InactivityCause
1450    {
1451        /**
1452         * Constant for excluded features
1453         */
1454        EXCLUDED,
1455        
1456        /**
1457         * Constant for features deactivated by other features
1458         */
1459        DEACTIVATED,
1460        
1461        /**
1462         * Constant for features disabled due to not choosen component
1463         */
1464        COMPONENT,
1465        
1466        /**
1467         * Constant for features disabled due to missing dependencies
1468         */
1469        DEPENDENCY,
1470        
1471        /**
1472         * Constant for passive features that are not necessary (nobody depends on it)
1473         */
1474        PASSIVE, 
1475        
1476        /**
1477         * Constant for features disabled to wrong referenced extension point
1478         */
1479        INVALID_POINT, 
1480        
1481        /**
1482         * Feature is not safe while in safe mode
1483         */
1484        NOT_SAFE
1485    }
1486    
1487    /**
1488     * PluginsManager status after initialization.
1489     */
1490    public enum Status
1491    {
1492        /**
1493         * Everything is ok. All features were correctly loaded.
1494         */
1495        OK,
1496        
1497        /**
1498         * There was no errors, but the configuration is missing.
1499         */
1500        NO_CONFIG,
1501        
1502        /**
1503         * There was no errors, but the configuration is incomplete.
1504         */
1505        CONFIG_INCOMPLETE,
1506        
1507        /**
1508         * Something was wrong when reading plugins definitions.
1509         */
1510        WRONG_DEFINITIONS,
1511        
1512        /**
1513         * There were issues during components loading.
1514         */
1515        NOT_INITIALIZED, 
1516        
1517        /**
1518         * The runtime.xml could not be loaded.
1519         */
1520        RUNTIME_NOT_LOADED,
1521        
1522        /**
1523         * Safe mode has been forced.
1524         */
1525        SAFE_MODE_FORCED
1526    }
1527    
1528    /**
1529     * Helper class containing all relevant informations after features list computation.
1530     */
1531    public static class PluginsInformation
1532    {
1533        private Map<String, Plugin> _plugins;
1534        private Map<String, Feature> _features;
1535        private Map<String, InactivityCause> _inactiveFeatures;
1536        private Map<String, ExtensionPointDefinition> _extensionPoints;
1537        private Map<String, Map<String, ExtensionDefinition>> _extensions;
1538        private Map<String, ComponentDefinition> _components;
1539        private Collection<PluginIssue> _errors;
1540        
1541        PluginsInformation(Map<String, Plugin> plugins, Map<String, Feature> features, Map<String, InactivityCause> inactiveFeatures, Map<String, ExtensionPointDefinition> extensionPoints, Map<String, Map<String, ExtensionDefinition>> extensions, Map<String, ComponentDefinition> components, Collection<PluginIssue> errors)
1542        {
1543            _plugins = plugins;
1544            _features = features;
1545            _inactiveFeatures = inactiveFeatures;
1546            _extensionPoints = extensionPoints;
1547            _extensions = extensions;
1548            _components = components;
1549            _errors = errors;
1550        }
1551        
1552        Map<String, Plugin> getPlugins()
1553        {
1554            return _plugins;
1555        }
1556        
1557        Map<String, Feature> getFeatures()
1558        {
1559            return _features;
1560        }
1561        
1562        Map<String, InactivityCause> getInactiveFeatures()
1563        {
1564            return _inactiveFeatures;
1565        }
1566        
1567        Map<String, ExtensionPointDefinition> getExtensionPoints()
1568        {
1569            return _extensionPoints;
1570        }
1571        
1572        Map<String, Map<String, ExtensionDefinition>> getExtensions()
1573        {
1574            return _extensions;
1575        }
1576        
1577        Map<String, ComponentDefinition> getComponents()
1578        {
1579            return _components;
1580        }
1581        
1582        /**
1583         * Returns all errors collected during initialization phase.
1584         * @return all errors collected during initialization phase.
1585         */
1586        public Collection<PluginIssue> getErrors()
1587        {
1588            return _errors;
1589        }
1590    }
1591}