001/*
002 *  Copyright 2018 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.util.ArrayList;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.LinkedHashMap;
023import java.util.Map;
024import java.util.Map.Entry;
025import java.util.Set;
026
027import org.apache.commons.collections.CollectionUtils;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031import org.ametys.runtime.plugin.PluginIssue.PluginIssueCode;
032import org.ametys.runtime.plugin.PluginsManager.InactivityCause;
033import org.ametys.runtime.servlet.RuntimeConfig;
034
035/**
036 * Basic impl
037 */
038public abstract class AbstractFeatureActivator implements FeatureActivator
039{
040    /** Logger */
041    protected final Logger _logger = LoggerFactory.getLogger(this.getClass());
042    /** Map of association identifier, plugin */
043    protected final Map<String, Plugin> _allPlugins;
044    /** true when in safe mode */
045    protected boolean _safeMode;
046    
047    AbstractFeatureActivator(Map<String, Plugin> allPlugins)
048    {
049        _allPlugins = allPlugins;
050    }
051    
052    /**
053     * Compute the active plugins
054     * @param excludedPlugins The excluded plugins
055     * @param initialFeatures The features
056     * @param inactiveFeatures The features that are inactive
057     * @param extensionPoints The extension points
058     * @param errors The issues
059     * @return The active plugins
060     */
061    protected Map<String, Plugin> computeActivePlugins(
062            Collection<String> excludedPlugins, 
063            Map<String, Feature> initialFeatures, 
064            Map<String, InactivityCause> inactiveFeatures, 
065            Map<String, ExtensionPointDefinition> extensionPoints, 
066            Collection<PluginIssue> errors)
067    {
068        Map<String, Plugin> plugins = new HashMap<>();
069        for (String pluginName : _allPlugins.keySet())
070        {
071            if (!excludedPlugins.contains(pluginName))
072            {
073                Plugin plugin = _allPlugins.get(pluginName);
074                plugins.put(pluginName, plugin);
075                _logger.info("Plugin '{}' loaded", pluginName);
076
077                // Check uniqueness of extension points
078                Map<String, ExtensionPointDefinition> extPoints = plugin.getExtensionPointDefinitions();
079                for (String point : extPoints.keySet())
080                {
081                    ExtensionPointDefinition definition = extPoints.get(point);
082                    
083                    if (!_safeMode || definition._safe)
084                    {
085                        if (extensionPoints.containsKey(point))
086                        {
087                            // It is an error to have two extension points with the same id, but we should not interrupt when in safe mode, so just ignore it
088                            String message = "The extension point '" + point + "', defined in the plugin '" + pluginName + "' is already defined in another plugin. ";
089                            PluginIssue issue = new PluginIssue(pluginName, null, PluginIssue.PluginIssueCode.EXTENSIONPOINT_ALREADY_EXIST, definition._configuration.getLocation(), message);
090    
091                            if (!_safeMode)
092                            {
093                                _logger.error(message);
094                                errors.add(issue);
095                            }
096                            else
097                            {
098                                _logger.debug("[Safe mode] {}", message);
099                            }
100                        }
101                        else
102                        {
103                            extensionPoints.put(point, definition);
104                        }
105                    }
106                }
107                
108                Map<String, Feature> features = plugin.getFeatures();
109                for (String id : features.keySet())
110                {
111                    Feature feature = features.get(id);
112                    
113                    if (!_safeMode || feature.isSafe())
114                    {
115                        initialFeatures.put(id, feature);
116                    }
117                    else
118                    {
119                        inactiveFeatures.put(id, InactivityCause.NOT_SAFE);
120                    }
121                }
122            }
123            else
124            {
125                _logger.debug("Plugin '{}' is excluded", pluginName);
126            }
127        }
128
129        return plugins;
130    }
131    
132    /**
133     * Compute incoming deactivations
134     * @param features The features
135     * @return The deactivations
136     */
137    protected Map<String, Collection<String>> computeIncomingDeactivations(Map<String, Feature> features)
138    {
139        Map<String, Collection<String>> incomingDeactivations = new HashMap<>();
140        
141        for (String id : features.keySet())
142        {
143            Feature feature = features.get(id);
144            Collection<String> deactivations = feature.getDeactivations();
145            
146            for (String deactivation : deactivations)
147            {
148                Collection<String> deps = incomingDeactivations.get(deactivation);
149                if (deps == null)
150                {
151                    deps = new ArrayList<>();
152                    incomingDeactivations.put(deactivation, deps);
153                }
154                
155                deps.add(id);
156            }
157        }
158        
159        return incomingDeactivations;
160    }
161    
162    /**
163     * Remove inactive features
164     * @param initialFeatures The initial features
165     * @param inactiveFeatures The inactive features
166     * @param incomingDeactivations The deactivations
167     * @param componentsConfig The components
168     */
169    protected void removeInactiveFeatures(
170            Map<String, Feature> initialFeatures, 
171            Map<String, InactivityCause> inactiveFeatures, 
172            Map<String, Collection<String>> incomingDeactivations, 
173            Map<String, String> componentsConfig)
174    {
175        Iterator<String> it = initialFeatures.keySet().iterator();
176        while (it.hasNext())
177        {
178            String id = it.next();
179            Feature feature = initialFeatures.get(id);
180            
181            if (incomingDeactivations.containsKey(id) && !incomingDeactivations.get(id).isEmpty())
182            {
183                String deactivatingFeature = incomingDeactivations.get(id).iterator().next();
184                _logger.debug("Removing feature {} deactivated by feature {}.", id, deactivatingFeature);
185                it.remove();
186                inactiveFeatures.put(id, InactivityCause.DEACTIVATED);
187                continue;
188            }
189            
190            Map<String, String> components = feature.getComponentsIds();
191            for (String role : components.keySet())
192            {
193                String componentId = components.get(role);
194                String selectedId = componentsConfig.get(role);
195                
196                // remove the feature if the user asked for a specific id and the declared component has not that id 
197                if (selectedId != null && !selectedId.equals(componentId))
198                {
199                    _logger.debug("Removing feature '{}' as it contains the component id '{}' for role '{}' but the user selected the id '{}' for that role.", id, componentId, role, selectedId);
200                    it.remove();
201                    inactiveFeatures.put(id, InactivityCause.COMPONENT);
202                    continue;
203                }
204            }
205        }
206    }
207    
208    /**
209     * Remove the wrong points
210     * @param initialFeatures The initial features
211     * @param inactiveFeatures The inactive features
212     * @param extensionPoints The extension points
213     * @param errors The errors
214     */
215    protected void removeWrongPointReferences(
216            Map<String, Feature> initialFeatures, 
217            Map<String, InactivityCause> inactiveFeatures, 
218            Map<String, ExtensionPointDefinition> extensionPoints, 
219            Collection<PluginIssue> errors)
220    {
221        Set<String> ids = initialFeatures.keySet();
222        Iterator<String> it = ids.iterator();
223        while (it.hasNext())
224        {
225            String id = it.next();
226            Feature feature = initialFeatures.get(id);
227            Map<String, Collection<String>> extensionsIds = feature.getExtensionsIds();
228            boolean hasBeenRemoved = false;
229            for (String point : extensionsIds.keySet())
230            {
231                if (!extensionPoints.containsKey(point))
232                {
233                    String message = "In feature '" + id + "' an extension references the non-existing point '" + point + "'.";
234                    _logger.error(message);
235                    PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.INVALID_POINT, null, message);
236                    errors.add(issue);
237                    if (!hasBeenRemoved)
238                    {
239                        it.remove();
240                        inactiveFeatures.put(id, InactivityCause.INVALID_POINT);
241                        hasBeenRemoved = true;
242                    }
243                }
244            }
245        }
246    }
247    
248    /**
249     * Process the outgoing dependencies
250     * @param initialFeatures The initial features
251     * @param inactiveFeatures The inactive features
252     * @param errors The errors
253     * @return the outgoing dependencies
254     */
255    protected Map<String, Feature> processOutgoingDependencies(
256            Map<String, Feature> initialFeatures, 
257            Map<String, InactivityCause> inactiveFeatures, 
258            Collection<PluginIssue> errors)
259    {
260        // Check outgoing dependencies
261        boolean processDependencies = true;
262        while (processDependencies)
263        {
264            processDependencies = false;
265            
266            Collection<String> ids = initialFeatures.keySet();
267            Iterator<String> it = ids.iterator();
268            while (it.hasNext())
269            {
270                String id = it.next();
271                Feature feature = initialFeatures.get(id);
272                Collection<String> dependencies = feature.getDependencies();
273                for (String dependency : dependencies)
274                {
275                    if (!initialFeatures.containsKey(dependency))
276                    {
277                        _logger.debug("The feature '{}' depends on '{}' which is not present. It will be ignored.", id, dependency);
278                        it.remove();
279                        inactiveFeatures.put(id, InactivityCause.DEPENDENCY);
280                        processDependencies = true;
281                        break; // end iterate over dependencies (avoid a potential second it.remove() which will throw exception) and continue the while loop
282                    }
283                }
284            }
285        }
286        
287        // Reorder remaining features, respecting dependencies
288        Map<String, Feature> resultFeatures = new LinkedHashMap<>();
289        
290        for (String featureId : initialFeatures.keySet())
291        {
292            _computeFeaturesDependencies(featureId, initialFeatures, resultFeatures, featureId, errors);
293        }
294        
295        return resultFeatures;
296    }
297    
298    private void _computeFeaturesDependencies(
299            String featureId, 
300            Map<String, Feature> initialFeatures, 
301            Map<String, Feature> resultFeatures, 
302            String initialFeatureId, 
303            Collection<PluginIssue> errors)
304    {
305        Feature feature = initialFeatures.get(featureId);
306        Collection<String> dependencies = feature.getDependencies();
307        
308        for (String dependency : dependencies)
309        {
310            if (initialFeatureId.equals(dependency))
311            {
312                String message = "Circular dependency detected for feature: " + feature.getFeatureId();
313                _logger.error(message);
314                PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.CIRCULAR_DEPENDENCY, null, message);
315                errors.add(issue);
316            }
317            else if (!resultFeatures.containsKey(dependency))
318            {
319                // do not process the feature if it has already been processed
320                _computeFeaturesDependencies(dependency, initialFeatures, resultFeatures, initialFeatureId, errors);
321            }
322        }
323        
324        resultFeatures.put(featureId, feature);
325    }
326    
327    /**
328     * Compute incoming dependencies
329     * @param features The features
330     * @return The dependencies
331     */
332    protected Map<String, Collection<String>> computeIncomingDependencies(Map<String, Feature> features)
333    {
334        Map<String, Collection<String>> incomingDependencies = new HashMap<>();
335        for (String id : features.keySet())
336        {
337            Feature feature = features.get(id);
338            Collection<String> dependencies = feature.getDependencies();
339            
340            for (String dependency : dependencies)
341            {
342                Collection<String> deps = incomingDependencies.get(dependency);
343                if (deps == null)
344                {
345                    deps = new ArrayList<>();
346                    incomingDependencies.put(dependency, deps);
347                }
348                
349                deps.add(id);
350            }
351        }
352        
353        return incomingDependencies;
354    }
355    
356    /**
357     * Compute the outgoing dependencies
358     * @param features The features
359     * @return The dependencies
360     */
361    protected Map<String, Collection<String>> computeOutgoingDependencies(Map<String, Feature> features)
362    {
363        Map<String, Collection<String>> outgoingDependencies = new HashMap<>();
364        for (String id : features.keySet())
365        {
366            Feature feature = features.get(id);
367            Collection<String> dependencies = feature.getDependencies();
368            outgoingDependencies.put(id, dependencies);
369        }
370        
371        return outgoingDependencies;
372    }
373    
374    /**
375     * Remove the unused features that were declared passive
376     * @param features The features
377     * @param inactiveFeatures The inactive features
378     * @param incomingDependencies The dependencies
379     */
380    protected void removeUnusedPassiveFeatures(
381            Map<String, Feature> features, 
382            Map<String, InactivityCause> inactiveFeatures, 
383            Map<String, Collection<String>> incomingDependencies)
384    {
385        Set<String> ids = features.keySet();
386        Iterator<String> it = ids.iterator();
387        while (it.hasNext())
388        {
389            String id = it.next();
390            Feature feature = features.get(id);
391            
392            if (feature.isPassive() && !incomingDependencies.containsKey(id))
393            {
394                _logger.debug("Remove passive feature '{}'", id);
395                it.remove();
396                inactiveFeatures.put(id, InactivityCause.PASSIVE);
397            }
398        }
399    }
400    
401    /**
402     * Compute the extensions
403     * @param features The features
404     * @param errors The errors
405     * @return The extensions
406     */
407    protected Map<String, Map<String, ExtensionDefinition>> computeExtensions(
408            Map<String, Feature> features, 
409            Collection<PluginIssue> errors)
410    {
411        Map<String, Map<String, ExtensionDefinition>> extensionsDefinitions = new HashMap<>();
412        for (Feature feature : features.values())
413        {
414            // extensions
415            Map<String, Map<String, ExtensionDefinition>> extensionsConfs = feature.getExtensions();
416            for (String point : extensionsConfs.keySet())
417            {
418                Map<String, ExtensionDefinition> featureExtensions = extensionsConfs.get(point);
419                Map<String, ExtensionDefinition> globalExtensions = extensionsDefinitions.get(point);
420                if (globalExtensions == null)
421                {
422                    globalExtensions = new LinkedHashMap<>(featureExtensions);
423                    extensionsDefinitions.put(point, globalExtensions);
424                }
425                else
426                {
427                    for (String id : featureExtensions.keySet())
428                    {
429                        if (globalExtensions.containsKey(id))
430                        {
431                            String message = "The extension '" + id + "' to point '" + point + "' is already defined in another feature.";
432                            _logger.error(message);
433                            PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.EXTENSION_ALREADY_EXIST, null, message);
434                            errors.add(issue);
435                        }
436                        else
437                        {
438                            ExtensionDefinition definition = featureExtensions.get(id);
439                            globalExtensions.put(id, definition);
440                        }
441                    }
442                }
443            }
444        }
445        
446        return extensionsDefinitions;
447    }
448    
449    /**
450     * compute the components
451     * @param features The features
452     * @param componentsConfig The component configurations
453     * @param errors The errors
454     * @return The components
455     */
456    protected Map<String, ComponentDefinition> computeComponents(
457            Map<String, Feature> features, 
458            Map<String, String> componentsConfig, 
459            Collection<PluginIssue> errors)
460    {
461        Map<String, ComponentDefinition> components = new HashMap<>();
462        
463        for (Feature feature : features.values())
464        {
465            // components
466            Map<String, ComponentDefinition> featureComponents = feature.getComponents();
467            for (String role : featureComponents.keySet())
468            {
469                ComponentDefinition definition = featureComponents.get(role);
470                ComponentDefinition globalDefinition = components.get(role);
471                if (globalDefinition == null)
472                {
473                    components.put(role, definition);
474                }
475                else
476                {
477                    String id = definition.getId();
478                    if (id.equals(globalDefinition.getId()))
479                    {
480                        String message = "The component for role '" + role + "' and id '" + id + "' is defined both in feature '" + definition.getPluginName() + PluginsManager.FEATURE_ID_SEPARATOR + definition.getFeatureName() + "' and in feature '" + globalDefinition.getPluginName() + PluginsManager.FEATURE_ID_SEPARATOR + globalDefinition.getFeatureName() + "'.";
481                        _logger.error(message);
482                        PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.COMPONENT_ALREADY_EXIST, null, message);
483                        errors.add(issue);
484                    }
485                    else
486                    {
487                        String message = "The component for role '" + role + "' is defined with id '" + id + "' in the feature '" + definition.getPluginName() + PluginsManager.FEATURE_ID_SEPARATOR + definition.getFeatureName() + "' and with id '" + globalDefinition.getId() + "' in the feature '" + globalDefinition.getPluginName() + PluginsManager.FEATURE_ID_SEPARATOR + globalDefinition.getFeatureName() + "'. One of them should be chosen in the runtime.xml.";
488                        _logger.error(message);
489                        PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.COMPONENT_ALREADY_EXIST, null, message);
490                        errors.add(issue);
491                    }
492                }
493            }
494        }
495        
496        // check that each component choosen in the runtime.xml is actually defined
497        for (String role : componentsConfig.keySet())
498        {
499            String requiredId = componentsConfig.get(role);
500            ComponentDefinition definition = components.get(role);
501            
502            if (definition == null || !definition.getId().equals(requiredId))
503            {
504                // Due to preceding checks, the definition id should not be different than requiredId, but two checks are always better than one ...
505                String message = "The component for role '" + role + "' should point to id '" + requiredId + "' but no component match.";
506                _logger.error(message);
507                PluginIssue issue = new PluginIssue(null, null, PluginIssueCode.COMPONENT_NOT_DECLARED, null, message);
508                errors.add(issue);
509            }
510        }
511        
512        return components;
513    }
514    
515    @Override
516    public String fullDump(PluginsInformation pluginInfo)
517    {
518        Map<String, InactivityCause> inactiveFeatures = pluginInfo.getInactiveFeatures();
519        Collection<PluginIssue> errors = pluginInfo.getErrors();
520        
521        Collection<String> excludedPlugins = RuntimeConfig.getInstance().getExcludedPlugins();
522        StringBuilder sb = new StringBuilder();
523        
524        for (String pluginName : _allPlugins.keySet())
525        {
526            Plugin plugin = _allPlugins.get(pluginName);
527            _dumpPlugin(sb, plugin, excludedPlugins, inactiveFeatures);
528        }
529        
530        if (!errors.isEmpty())
531        {
532            sb.append("\nErrors :\n");
533            errors.forEach(issue -> sb.append(issue.toString()).append('\n'));
534        }
535        
536        return sb.toString();
537    }
538    
539    private void _dumpPlugin(StringBuilder sb, Plugin plugin, Collection<String> excludedPlugins, Map<String, InactivityCause> inactiveFeatures)
540    {
541        String pluginName = plugin.getName();
542        sb.append("Plugin ").append(pluginName);
543        
544        if (excludedPlugins.contains(pluginName))
545        {
546            sb.append("   *** excluded ***");
547        }
548        
549        sb.append('\n');
550        
551        Collection<String> configParameters = plugin.getConfigParameters().keySet();
552        if (!CollectionUtils.isEmpty(configParameters))
553        {
554            sb.append("  Config parameters : \n");
555            configParameters.forEach(param -> sb.append("    ").append(param).append('\n'));
556        }
557        
558        Collection<String> paramCheckers = plugin.getParameterCheckers().keySet();
559        if (!CollectionUtils.isEmpty(paramCheckers))
560        {
561            sb.append("  Parameters checkers : \n");
562            paramCheckers.forEach(param -> sb.append("    ").append(param).append('\n'));
563        }
564        
565        Collection<String> extensionPoints = plugin.getExtensionPoints();
566        if (!CollectionUtils.isEmpty(extensionPoints))
567        {
568            sb.append("  Extension points : \n");
569            extensionPoints.forEach(point -> sb.append("    ").append(point).append('\n'));
570        }
571        
572        Map<String, Feature> features = plugin.getFeatures();
573        for (String featureId : features.keySet())
574        {
575            Feature feature = features.get(featureId);
576            _dumpFeature(sb, feature, inactiveFeatures);
577        }
578        
579        sb.append('\n');
580    }
581    
582    private void _dumpFeature(StringBuilder sb, Feature feature, Map<String, InactivityCause> inactiveFeatures)
583    {
584        String featureId = feature.getFeatureId();
585        
586        sb.append("  Feature ").append(featureId);
587        if (feature.isPassive())
588        {
589            sb.append(" (passive)");
590        }
591
592        if (feature.isSafe())
593        {
594            sb.append(" (safe)");
595        }
596        
597        if (inactiveFeatures != null && inactiveFeatures.containsKey(featureId))
598        {
599            sb.append("   *** inactive (").append(inactiveFeatures.get(featureId)).append(") ***");
600        }
601        
602        sb.append('\n');
603        
604        Collection<String> featureConfigParameters = feature.getConfigParameters().keySet();
605        if (!CollectionUtils.isEmpty(featureConfigParameters))
606        {
607            sb.append("    Config parameters : \n");
608            featureConfigParameters.forEach(param -> sb.append("      ").append(param).append('\n'));
609        }
610        
611        Collection<String> configParametersReferences = feature.getConfigParametersReferences();
612        if (!CollectionUtils.isEmpty(configParametersReferences))
613        {
614            sb.append("    Config parameters references : \n");
615            configParametersReferences.forEach(param -> sb.append("      ").append(param).append('\n'));
616        }
617        
618        Collection<String> featureParamCheckers = feature.getParameterCheckers().keySet();
619        if (!CollectionUtils.isEmpty(featureParamCheckers))
620        {
621            sb.append("    Parameters checkers : \n");
622            featureParamCheckers.forEach(param -> sb.append("    ").append(param).append('\n'));
623        }
624        
625        Map<String, String> componentsIds = feature.getComponentsIds();
626        if (!componentsIds.isEmpty())
627        {
628            sb.append("    Components : \n");
629            
630            for (String role : componentsIds.keySet())
631            {
632                String id = componentsIds.get(role);
633                sb.append("      ").append(role).append(" : ").append(id).append('\n');
634            }
635            
636            sb.append('\n');
637        }
638
639        Map<String, Collection<String>> extensionsIds = feature.getExtensionsIds();
640        if (!extensionsIds.isEmpty())
641        {
642            sb.append("    Extensions : \n");
643            
644            for (Entry<String, Collection<String>> extensionEntry : extensionsIds.entrySet())
645            {
646                String point = extensionEntry.getKey();
647                Collection<String> ids = extensionEntry.getValue();
648                
649                sb.append("      ").append(point).append(" :\n");
650                ids.forEach(id -> sb.append("        ").append(id).append('\n'));
651            }
652            
653            sb.append('\n');
654        }
655    }
656}