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