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