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