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}