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.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.stream.Collectors;
027import java.util.stream.Stream;
028
029import org.apache.commons.collections4.ListUtils;
030
031import org.ametys.runtime.plugin.PluginsManager.InactivityCause;
032
033/**
034 * {@link FeatureActivator} which activates only passed included features and their dependencies.
035 */
036public final class IncludePolicyFeatureActivator extends AbstractFeatureActivator
037{
038    static String[] __KERNEL_DEPENDENCIES_NEEDED = new String[]
039    {
040        "core/core.observation", // CocoonWrapper needs ObservationManager
041        "core/core.migration" // CocoonWrapper needs MigrationEngine
042    };
043    
044    private Collection<IncludedFeature> _includedFeatures;
045    
046    /**
047     * Constructs a new feature activator with an 'include' policy
048     * @param allPlugins all plugins
049     * @param includedFeatures the features to include
050     */
051    public IncludePolicyFeatureActivator(Map<String, Plugin> allPlugins, Collection<IncludedFeature> includedFeatures)
052    {
053        super(allPlugins);
054        _includedFeatures = includedFeatures;
055    }
056
057    @Override
058    public PluginsInformation computeActiveFeatures(Map<String, String> componentsConfig, boolean safeMode)
059    {
060        _safeMode = safeMode;
061        Map<String, Feature> initialFeatures = new HashMap<>();
062        Map<String, ExtensionPointDefinition> extensionPoints = new HashMap<>();
063        Map<String, InactivityCause> inactiveFeatures = new HashMap<>();
064        Collection<PluginIssue> errors = new ArrayList<>();
065        
066        // Get actual plugin list, corresponding extension points and initial feature list
067        Map<String, Plugin> plugins = computeActivePlugins(Collections.emptySet(), initialFeatures, inactiveFeatures, extensionPoints, errors);
068        
069        removeWrongPointReferences(initialFeatures, inactiveFeatures, extensionPoints, errors);
070
071        Map<String, Collection<IncomingDeactivation>> incomingDeactivations = computeIncomingDeactivations(initialFeatures);
072        
073        // check consistency of given included features (it can lead to a paradox if one of them triggers by its dependencies the deactivation of another one)
074        new IncludedFeatureConsistencyChecker(_includedFeatures, initialFeatures, incomingDeactivations).checkConsistency();
075        
076        _correctedDependencies = new CorrectedDependencies(initialFeatures, incomingDeactivations, _logger);
077        
078        // Process outgoing dependencies
079        Map<String, Feature> activeFeatures = processOutgoingDependencies(initialFeatures, inactiveFeatures, errors);
080
081        // Compute outgoing dependencies
082        Map<String, Collection<String>> outgoingDependencies = computeOutgoingDependencies(activeFeatures);
083        
084        // Get included features, get its dependencies, remove unused features, deactivate features and finally remove dependencies becoming unused 
085        List<String> includedFeatures = _getIncludedFeatures();
086        List<String> dependencies = _getDependencies(includedFeatures, outgoingDependencies);
087        List<String> includedFeaturesAndDependencies = ListUtils.union(includedFeatures, dependencies);
088        _removeUnusedFeatures(activeFeatures, inactiveFeatures, includedFeaturesAndDependencies);
089        Map<String, Collection<IncomingDeactivation>> featuresToDeactivate = _getFeaturesToDeactivate(incomingDeactivations, includedFeaturesAndDependencies);
090        List<String> dependenciesNowUnused = _computeDependenciesNowUnused(featuresToDeactivate.keySet(), outgoingDependencies, includedFeatures, activeFeatures);
091        _removeDeactivatedFeatures(activeFeatures, inactiveFeatures, featuresToDeactivate, dependenciesNowUnused);
092        
093        // Check uniqueness of extensions and components
094        Map<String, Map<String, ExtensionDefinition>> extensions = computeExtensions(activeFeatures, errors);
095        Map<String, ComponentDefinition> components = computeComponents(activeFeatures, componentsConfig, errors);
096        
097        return new PluginsInformation(plugins, activeFeatures, inactiveFeatures, extensionPoints, extensions, components, errors);
098    }
099    
100    private List<String> _getIncludedFeatures()
101    {
102        return Stream.concat(
103                   Stream.of(__KERNEL_DEPENDENCIES_NEEDED), 
104                   _includedFeatures.stream()
105                       .map(IncludedFeature::featureId)
106               )
107               .collect(Collectors.toList());
108    }
109    
110    private List<String> _getDependencies(List<String> featuresToProcess, Map<String, Collection<String>> outgoingDependencies)
111    {
112        List<String> dependencies = new ArrayList<>();
113        _fillRecursiveDependencies(featuresToProcess, outgoingDependencies, dependencies, false);
114        return dependencies;
115    }
116    
117    private void _fillRecursiveDependencies(Collection<String> featuresToProcess, Map<String, Collection<String>> outgoingDependencies, List<String> dependencyListToFill, boolean addFeaturesToProcess)
118    {
119        for (String featureToProcess : featuresToProcess)
120        {
121            if (addFeaturesToProcess)
122            {
123                dependencyListToFill.add(featureToProcess);
124            }
125            Collection<String> deps = outgoingDependencies.get(featureToProcess);
126            if (deps != null)
127            {
128                _fillRecursiveDependencies(deps, outgoingDependencies, dependencyListToFill, true);
129            }
130        }
131    }
132    
133    private void _removeUnusedFeatures(
134            Map<String, Feature> activeFeatures, 
135            Map<String, InactivityCause> inactiveFeatures, 
136            Collection<String> featuresToKeep)
137    {
138        Set<String> ids = activeFeatures.keySet();
139        Iterator<String> it = ids.iterator();
140        while (it.hasNext())
141        {
142            String id = it.next();
143            
144            if (!featuresToKeep.contains(id))
145            {
146                _logger.info("Remove unused feature '{}'", id);
147                it.remove();
148                if (!inactiveFeatures.containsKey(id))
149                {
150                    inactiveFeatures.put(id, InactivityCause.UNUSED);
151                }
152            }
153        }
154    }
155    
156    private Map<String, Collection<IncomingDeactivation>> _getFeaturesToDeactivate(Map<String, Collection<IncomingDeactivation>> incomingDeactivations, List<String> includedFeatures)
157    {
158        Map<String, Collection<IncomingDeactivation>> result = new HashMap<>();
159        for (String id : includedFeatures)
160        {
161            if (incomingDeactivations.containsKey(id))
162            {
163                Collection<IncomingDeactivation> deactivatedBy = incomingDeactivations.get(id);
164                if (IncomingDeactivation.containsAny(includedFeatures, deactivatedBy))
165                {
166                    result.put(id, IncomingDeactivation.intersection(includedFeatures, deactivatedBy));
167                }
168            }
169        }
170        return result;
171    }
172    
173    private List<String> _computeDependenciesNowUnused(
174            Collection<String> featuresToDeactivate, 
175            Map<String, Collection<String>> outgoingDependencies,
176            List<String> includedFeatures,
177            Map<String, Feature> allFeatures)
178    {
179        List<String> dependenciesNowUnused = new ArrayList<>();
180        
181        Map<String, Collection<String>> incomingDependencies = computeIncomingDependencies(allFeatures);
182        for (String featureToDeactivate : featuresToDeactivate)
183        {
184            Collection<String> dependencies = outgoingDependencies.get(featureToDeactivate);
185            // Check it is only needed by the feature which will be deactivated
186            for (String dependency : dependencies)
187            {
188                Collection<String> incomingDependency = incomingDependencies.get(dependency);
189                if (incomingDependency.size() == 1 && incomingDependency.iterator().next().equals(featureToDeactivate) && !includedFeatures.contains(dependency))
190                {
191                    dependenciesNowUnused.add(dependency);
192                }
193            }
194        }
195        
196        return dependenciesNowUnused;
197    }
198    
199    private void _removeDeactivatedFeatures(Map<String, Feature> activeFeatures, Map<String, InactivityCause> inactiveFeatures, Map<String, Collection<IncomingDeactivation>> featuresToDeactivate, List<String> dependenciesNowUnused)
200    {
201        Iterator<String> it = activeFeatures.keySet().iterator();
202        while (it.hasNext())
203        {
204            String id = it.next();
205            if (featuresToDeactivate.containsKey(id))
206            {
207                Collection<IncomingDeactivation> deactivatedBy = featuresToDeactivate.get(id);
208                _logger.debug("Remove feature {} deactivated by features {}", id, deactivatedBy);
209                it.remove();
210                InactivityCause cause = _getCause(deactivatedBy);
211                inactiveFeatures.put(id, cause);
212            }
213            else if (dependenciesNowUnused.contains(id))
214            {
215                _logger.debug("Remove unused dependency '{}'", id);
216                it.remove();
217                if (!inactiveFeatures.containsKey(id))
218                {
219                    inactiveFeatures.put(id, InactivityCause.UNUSED);
220                }
221            }
222        }
223    }
224    
225    private static InactivityCause _getCause(Collection<IncomingDeactivation> deactivatedBy)
226    {
227        if (deactivatedBy.isEmpty())
228        {
229            throw new IllegalStateException("collection 'deactivatedBy' cannot be empty");
230        }
231        else if (deactivatedBy.size() == 1)
232        {
233            return deactivatedBy.iterator().next().getType().getInactivityCause();
234        }
235        else
236        {
237            return deactivatedBy.stream()
238                    .map(IncomingDeactivation::getType)
239                    .map(IncomingDeactivation.Type::getInactivityCause)
240                    .filter(InactivityCause.OVERRIDDEN::equals) // if at least one is overridden, consider that this is the main cause of inactivity
241                    .findAny()
242                    .orElse(InactivityCause.DEACTIVATED);
243            
244        }
245    }
246    
247    @Override
248    public String shortDump(PluginsInformation pluginInfo)
249    {
250        return new LoadedFeaturesDump(this).shortDump(pluginInfo, _includedFeatures);
251    }
252    
253    /**
254     * A feature to include by {@link IncludePolicyFeatureActivator}
255     */
256    public static final class IncludedFeature
257    {
258        private String _featureId;
259        private String _cause;
260        
261        private IncludedFeature(String featureId, String cause)
262        {
263            _featureId = featureId;
264            _cause = cause;
265        }
266        
267        /**
268         * Creates an {@link IncludedFeature}
269         * @param featureId The id of the feature
270         * @param cause The cause of its inclusion
271         * @return the feature to include
272         */
273        public static IncludedFeature of(String featureId, String cause)
274        {
275            return new IncludedFeature(featureId, cause);
276        }
277        
278        /**
279         * Gets the feature id
280         * @return the feature id
281         */
282        public String featureId()
283        {
284            return _featureId;
285        }
286        
287        /**
288         * Gets the cause of the inclusion of the feature
289         * @return the cause of the inclusion of the feature
290         */
291        public String cause()
292        {
293            return _cause;
294        }
295        
296        @Override
297        public String toString()
298        {
299            return String.format("%s [included by %s]", _featureId, _cause);
300        }
301    }
302}