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}