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