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.Optional;
026import java.util.Set;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029
030import org.apache.commons.collections4.CollectionUtils;
031import org.apache.commons.collections4.ListUtils;
032import org.apache.commons.lang3.ArrayUtils;
033
034import org.ametys.runtime.plugin.PluginsManager.InactivityCause;
035
036/**
037 * {@link FeatureActivator} which activates only passed included features and their dependencies.
038 */
039public final class IncludePolicyFeatureActivator extends AbstractFeatureActivator
040{
041    private static String[] __KERNEL_DEPENDENCIES_NEEDED = new String[]
042    {
043        "core/core.observation" // CocoonWrapper needs ObservationManager
044    };
045    
046    private Collection<IncludedFeature> _includedFeatures;
047    
048    /**
049     * Constructs a new feature activator with an 'include' policy
050     * @param allPlugins all plugins
051     * @param includedFeatures the features to include
052     */
053    public IncludePolicyFeatureActivator(Map<String, Plugin> allPlugins, Collection<IncludedFeature> includedFeatures)
054    {
055        super(allPlugins);
056        _includedFeatures = includedFeatures;
057    }
058
059    @Override
060    public PluginsInformation computeActiveFeatures(Map<String, String> componentsConfig, boolean safeMode)
061    {
062        _safeMode = safeMode;
063        Map<String, Feature> initialFeatures = new HashMap<>();
064        Map<String, ExtensionPointDefinition> extensionPoints = new HashMap<>();
065        Map<String, InactivityCause> inactiveFeatures = new HashMap<>();
066        Collection<PluginIssue> errors = new ArrayList<>();
067        
068        // Get actual plugin list, corresponding extension points and initial feature list
069        Map<String, Plugin> plugins = computeActivePlugins(Collections.emptySet(), initialFeatures, inactiveFeatures, extensionPoints, errors);
070        
071        removeWrongPointReferences(initialFeatures, inactiveFeatures, extensionPoints, errors);
072
073        // Process outgoing dependencies
074        Map<String, Feature> activeFeatures = processOutgoingDependencies(initialFeatures, inactiveFeatures, errors);
075
076        // Compute outgoing dependencies
077        Map<String, Collection<String>> outgoingDependencies = computeOutgoingDependencies(activeFeatures);
078        
079        // Get included features, get its dependencies, remove unused features, deactivate features and finally remove dependencies becoming unused 
080        List<String> includedFeatures = _getIncludedFeatures();
081        Map<String, Collection<String>> incomingDeactivations = computeIncomingDeactivations(initialFeatures);
082        List<String> dependencies = _getDependencies(includedFeatures, outgoingDependencies);
083        List<String> includedFeaturesAndDependencies = ListUtils.union(includedFeatures, dependencies);
084        _removeUnusedFeatures(activeFeatures, inactiveFeatures, includedFeaturesAndDependencies);
085        Map<String, Collection<String>> featuresToDeactivate = _getFeaturesToDeactivate(incomingDeactivations, includedFeaturesAndDependencies);
086        List<String> dependenciesNowUnused = _computeDependenciesNowUnused(featuresToDeactivate.keySet(), outgoingDependencies, includedFeatures, activeFeatures);
087        _removeDeactivatedFeatures(activeFeatures, inactiveFeatures, featuresToDeactivate, dependenciesNowUnused);
088        
089        // Check uniqueness of extensions and components
090        Map<String, Map<String, ExtensionDefinition>> extensions = computeExtensions(activeFeatures, errors);
091        Map<String, ComponentDefinition> components = computeComponents(activeFeatures, componentsConfig, errors);
092        
093        return new PluginsInformation(plugins, activeFeatures, inactiveFeatures, extensionPoints, extensions, components, errors);
094    }
095    
096    private List<String> _getIncludedFeatures()
097    {
098        return Stream.concat(
099                   Stream.of(__KERNEL_DEPENDENCIES_NEEDED), 
100                   _includedFeatures.stream()
101                       .map(IncludedFeature::featureId)
102               )
103               .collect(Collectors.toList());
104    }
105    
106    private List<String> _getDependencies(List<String> featuresToProcess, Map<String, Collection<String>> outgoingDependencies)
107    {
108        List<String> dependencies = new ArrayList<>();
109        _fillRecursiveDependencies(featuresToProcess, outgoingDependencies, dependencies, false);
110        return dependencies;
111    }
112    
113    private void _fillRecursiveDependencies(Collection<String> featuresToProcess, Map<String, Collection<String>> outgoingDependencies, List<String> dependencyListToFill, boolean addFeaturesToProcess)
114    {
115        for (String featureToProcess : featuresToProcess)
116        {
117            if (addFeaturesToProcess)
118            {
119                dependencyListToFill.add(featureToProcess);
120            }
121            Collection<String> deps = outgoingDependencies.get(featureToProcess);
122            if (deps != null)
123            {
124                _fillRecursiveDependencies(deps, outgoingDependencies, dependencyListToFill, true);
125            }
126        }
127    }
128    
129    private void _removeUnusedFeatures(
130            Map<String, Feature> activeFeatures, 
131            Map<String, InactivityCause> inactiveFeatures, 
132            Collection<String> featuresToKeep)
133    {
134        Set<String> ids = activeFeatures.keySet();
135        Iterator<String> it = ids.iterator();
136        while (it.hasNext())
137        {
138            String id = it.next();
139            
140            if (!featuresToKeep.contains(id))
141            {
142                _logger.debug("Remove unused feature '{}'", id);
143                it.remove();
144                if (!inactiveFeatures.containsKey(id))
145                {
146                    inactiveFeatures.put(id, InactivityCause.UNUSED);
147                }
148            }
149        }
150    }
151    
152    private Map<String, Collection<String>> _getFeaturesToDeactivate(Map<String, Collection<String>> incomingDeactivations, List<String> includedFeatures)
153    {
154        Map<String, Collection<String>> result = new HashMap<>();
155        for (String id : includedFeatures)
156        {
157            if (incomingDeactivations.containsKey(id))
158            {
159                Collection<String> deactivatedBy = incomingDeactivations.get(id);
160                if (deactivatedBy != null && CollectionUtils.containsAny(includedFeatures, deactivatedBy))
161                {
162                    result.put(id, CollectionUtils.intersection(includedFeatures, deactivatedBy));
163                }
164            }
165        }
166        return result;
167    }
168    
169    private List<String> _computeDependenciesNowUnused(
170            Collection<String> featuresToDeactivate, 
171            Map<String, Collection<String>> outgoingDependencies,
172            List<String> includedFeatures,
173            Map<String, Feature> allFeatures)
174    {
175        List<String> dependenciesNowUnused = new ArrayList<>();
176        
177        Map<String, Collection<String>> incomingDependencies = computeIncomingDependencies(allFeatures);
178        for (String featureToDeactivate : featuresToDeactivate)
179        {
180            Collection<String> dependencies = outgoingDependencies.get(featureToDeactivate);
181            // Check it is only needed by the feature which will be deactivated
182            for (String dependency : dependencies)
183            {
184                Collection<String> incomingDependency = incomingDependencies.get(dependency);
185                if (incomingDependency.size() == 1 && incomingDependency.iterator().next().equals(featureToDeactivate) && !includedFeatures.contains(dependency))
186                {
187                    dependenciesNowUnused.add(dependency);
188                }
189            }
190        }
191        
192        return dependenciesNowUnused;
193    }
194    
195    private void _removeDeactivatedFeatures(Map<String, Feature> activeFeatures, Map<String, InactivityCause> inactiveFeatures, Map<String, Collection<String>> featuresToDeactivate, List<String> dependenciesNowUnused)
196    {
197        Iterator<String> it = activeFeatures.keySet().iterator();
198        while (it.hasNext())
199        {
200            String id = it.next();
201            if (featuresToDeactivate.containsKey(id))
202            {
203                Collection<String> deactivatedBy = featuresToDeactivate.get(id);
204                _logger.debug("Remove feature {} deactivated by features {}", id, deactivatedBy);
205                it.remove();
206                inactiveFeatures.put(id, InactivityCause.DEACTIVATED);
207            }
208            else if (dependenciesNowUnused.contains(id))
209            {
210                _logger.debug("Remove unused dependency '{}'", id);
211                it.remove();
212                if (!inactiveFeatures.containsKey(id))
213                {
214                    inactiveFeatures.put(id, InactivityCause.UNUSED);
215                }
216            }
217        }
218    }
219    
220    @Override
221    public String shortDump(PluginsInformation pluginInfo)
222    {
223        Collection<PluginIssue> errors = pluginInfo.getErrors();
224        Map<String, InactivityCause> inactiveFeatures = pluginInfo.getInactiveFeatures();
225        Map<String, Collection<String>> dependencies = computeIncomingDependencies(pluginInfo.getFeatures());
226        StringBuilder sb = new StringBuilder();
227        
228        String separator = new String(new char[30]).replace('\0', '-');
229        sb.append(separator).append("\n");
230        
231        List<String> pluginNames = _allPlugins.keySet()
232                   .stream()
233                   .sorted()
234                   .collect(Collectors.toList());
235        for (String pluginName : pluginNames)
236        {
237            Plugin plugin = _allPlugins.get(pluginName);
238            _dumpPlugin(sb, plugin, dependencies, inactiveFeatures);
239        }
240        
241        if (!errors.isEmpty())
242        {
243            sb.append("\nErrors :\n");
244            errors.forEach(issue -> sb.append(issue.toString()).append('\n'));
245        }
246        
247        sb.append(separator).append("\n");
248        
249        return sb.toString();
250    }
251    
252    private void _dumpPlugin(StringBuilder sb, Plugin plugin, Map<String, Collection<String>> dependencies, Map<String, InactivityCause> inactiveFeatures)
253    {
254        String pluginName = plugin.getName();
255        sb.append("Plugin ").append(pluginName);
256        
257        sb.append("\n\n");
258        
259        Map<String, Feature> activeFeatures = plugin.getFeatures();
260        List<String> featureIds = activeFeatures.keySet()
261                .stream()
262                .sorted()
263                .collect(Collectors.toList());
264        for (String featureId : featureIds)
265        {
266            Feature feature = activeFeatures.get(featureId);
267            _dumpFeature(sb, feature, dependencies, inactiveFeatures);
268        }
269        
270        sb.append('\n');
271    }
272    
273    private void _dumpFeature(StringBuilder sb, Feature feature, Map<String, Collection<String>> dependencies, Map<String, InactivityCause> inactiveFeatures)
274    {
275        String featureId = feature.getFeatureId();
276        
277        if (!inactiveFeatures.containsKey(featureId))
278        {
279            sb.append("  Feature ").append(featureId);
280            List<String> dependenciesOfFeature = new ArrayList<>();
281            if (dependencies.containsKey(featureId))
282            {
283                dependenciesOfFeature.addAll(dependencies.get(featureId));
284            }
285            Optional<IncludedFeature> includedFeature = _find(featureId);
286            if (includedFeature.isPresent())
287            {
288                dependenciesOfFeature.add(0, "INCLUDED_FEATURE BY " + includedFeature.get().cause());
289            }
290            if (ArrayUtils.contains(__KERNEL_DEPENDENCIES_NEEDED, featureId))
291            {
292                dependenciesOfFeature.add(0, "KERNEL");
293            }
294            
295            if (!dependenciesOfFeature.isEmpty())
296            {
297                sb.append("\n    --> brought by ").append(dependenciesOfFeature);
298            }
299            
300            sb.append("\n\n");
301        }
302    }
303    
304    private Optional<IncludedFeature> _find(String featureId)
305    {
306        return _includedFeatures.stream()
307                .filter(f -> featureId.equals(f.featureId()))
308                .findFirst();
309    }
310    
311    /**
312     * A feature to include by {@link IncludePolicyFeatureActivator}
313     */
314    public static final class IncludedFeature
315    {
316        private String _featureId;
317        private String _cause;
318        
319        private IncludedFeature(String featureId, String cause)
320        {
321            _featureId = featureId;
322            _cause = cause;
323        }
324        
325        /**
326         * Creates an {@link IncludedFeature}
327         * @param featureId The id of the feature
328         * @param cause The cause of its inclusion
329         * @return the feature to include
330         */
331        public static IncludedFeature of(String featureId, String cause)
332        {
333            return new IncludedFeature(featureId, cause);
334        }
335        
336        /**
337         * Gets the feature id
338         * @return the feature id
339         */
340        public String featureId()
341        {
342            return _featureId;
343        }
344        
345        /**
346         * Gets the cause of the inclusion of the feture
347         * @return the cause of the inclusion of the feture
348         */
349        public String cause()
350        {
351            return _cause;
352        }
353    }
354}