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