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