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