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}