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.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027import java.util.Set; 028import java.util.stream.Collectors; 029import java.util.stream.Stream; 030 031import org.apache.commons.collections4.ListUtils; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035import org.ametys.runtime.plugin.IncomingDeactivation.Type; 036import org.ametys.runtime.plugin.PluginIssue.PluginIssueCode; 037import org.ametys.runtime.plugin.PluginsManager.InactivityCause; 038 039/** 040 * Basic impl 041 */ 042public abstract class AbstractFeatureActivator implements FeatureActivator 043{ 044 /** Logger */ 045 protected final Logger _logger = LoggerFactory.getLogger(this.getClass()); 046 /** Map of association identifier, plugin */ 047 protected final Map<String, Plugin> _allPlugins; 048 /** true when in safe mode */ 049 protected boolean _safeMode; 050 /** the computed and corrected dependencies */ 051 protected CorrectedDependencies _correctedDependencies; 052 053 AbstractFeatureActivator(Map<String, Plugin> allPlugins) 054 { 055 _allPlugins = allPlugins; 056 } 057 058 /** 059 * Compute the active plugins 060 * @param excludedPlugins The excluded plugins 061 * @param initialFeatures The features 062 * @param inactiveFeatures The features that are inactive 063 * @param extensionPoints The extension points 064 * @param errors The issues 065 * @return The active plugins 066 */ 067 protected Map<String, Plugin> computeActivePlugins( 068 Collection<String> excludedPlugins, 069 Map<String, Feature> initialFeatures, 070 Map<String, InactivityCause> inactiveFeatures, 071 Map<String, ExtensionPointDefinition> extensionPoints, 072 Collection<PluginIssue> errors) 073 { 074 Map<String, Plugin> plugins = new HashMap<>(); 075 for (String pluginName : _allPlugins.keySet()) 076 { 077 if (!excludedPlugins.contains(pluginName)) 078 { 079 Plugin plugin = _allPlugins.get(pluginName); 080 plugins.put(pluginName, plugin); 081 _logger.info("Plugin '{}' loaded", pluginName); 082 083 // Check uniqueness of extension points 084 Map<String, ExtensionPointDefinition> extPoints = plugin.getExtensionPointDefinitions(); 085 for (String point : extPoints.keySet()) 086 { 087 ExtensionPointDefinition definition = extPoints.get(point); 088 089 if (!_safeMode || definition._safe) 090 { 091 if (extensionPoints.containsKey(point)) 092 { 093 // It is an error to have two extension points with the same id, but we should not interrupt when in safe mode, so just ignore it 094 String message = "The extension point '" + point + "', defined in the plugin '" + pluginName + "' is already defined in another plugin. "; 095 PluginIssue issue = new PluginIssue(pluginName, null, PluginIssue.PluginIssueCode.EXTENSIONPOINT_ALREADY_EXIST, definition._configuration.getLocation(), message); 096 097 if (!_safeMode) 098 { 099 _logger.error(message); 100 errors.add(issue); 101 } 102 else 103 { 104 _logger.debug("[Safe mode] {}", message); 105 } 106 } 107 else 108 { 109 extensionPoints.put(point, definition); 110 } 111 } 112 } 113 114 Map<String, Feature> features = plugin.getFeatures(); 115 for (String id : features.keySet()) 116 { 117 Feature feature = features.get(id); 118 119 if (!_safeMode || feature.isSafe()) 120 { 121 initialFeatures.put(id, feature); 122 } 123 else 124 { 125 inactiveFeatures.put(id, InactivityCause.NOT_SAFE); 126 } 127 } 128 } 129 else 130 { 131 _logger.debug("Plugin '{}' is excluded", pluginName); 132 } 133 } 134 135 return plugins; 136 } 137 138 /** 139 * Compute incoming deactivations 140 * @param features The features 141 * @return The deactivations 142 */ 143 protected Map<String, Collection<IncomingDeactivation>> computeIncomingDeactivations(Map<String, Feature> features) 144 { 145 Map<String, Collection<IncomingDeactivation>> incomingDeactivations = new HashMap<>(); 146 147 for (String id : features.keySet()) 148 { 149 Feature feature = features.get(id); 150 Collection<String> deactivations = feature.getDeactivations(); 151 _fillDeactivationsByFeature(id, deactivations, incomingDeactivations); 152 Collection<String> overrides = feature.getOverrides(); 153 _fillOverridesByFeature(id, overrides, incomingDeactivations); 154 } 155 156 _checkNoMultipleOverriders(incomingDeactivations); 157 158 return incomingDeactivations; 159 } 160 161 private void _fillDeactivationsByFeature(String featureIdTriggeringDeactivation, Collection<String> deactivations, Map<String, Collection<IncomingDeactivation>> incomingDeactivations) 162 { 163 for (String deactivatedFeature : deactivations) 164 { 165 Collection<IncomingDeactivation> deps = incomingDeactivations.computeIfAbsent(deactivatedFeature, __ -> new ArrayList<>()); 166 deps.add(new IncomingDeactivation(Type.DEACTIVATED, featureIdTriggeringDeactivation)); 167 } 168 } 169 170 private void _fillOverridesByFeature(String featureIdTriggeringOverride, Collection<String> overrides, Map<String, Collection<IncomingDeactivation>> incomingDeactivations) 171 { 172 for (String overriddenFeature : overrides) 173 { 174 Collection<IncomingDeactivation> deps = incomingDeactivations.computeIfAbsent(overriddenFeature, __ -> new ArrayList<>()); 175 deps.add(new IncomingDeactivation(Type.OVERRIDDEN, featureIdTriggeringOverride)); 176 } 177 } 178 179 private void _checkNoMultipleOverriders(Map<String, Collection<IncomingDeactivation>> incomingDeactivations) 180 { 181 for (Map.Entry<String, Collection<IncomingDeactivation>> incomingDeactivation : incomingDeactivations.entrySet()) 182 { 183 Collection<IncomingDeactivation> overriders = incomingDeactivation.getValue() 184 .stream() 185 .filter(deactivator -> deactivator.getType() == Type.OVERRIDDEN) 186 .collect(Collectors.toList()); 187 if (overriders.size() > 1) 188 { 189 String overriddenFeature = incomingDeactivation.getKey(); 190 // has 2 or more overrides 191 throw new IllegalStateException(String.format( 192 "Feature '%s' is overridden by %s. It cannot be overridden by 2 features. The application will not be able to boot.", 193 overriddenFeature, overriders)); 194 } 195 } 196 } 197 198 /** 199 * Remove inactive features 200 * @param initialFeatures The initial features 201 * @param inactiveFeatures The inactive features 202 * @param incomingDeactivations The deactivations 203 * @param componentsConfig The components 204 */ 205 protected void removeInactiveFeatures( 206 Map<String, Feature> initialFeatures, 207 Map<String, InactivityCause> inactiveFeatures, 208 Map<String, Collection<IncomingDeactivation>> incomingDeactivations, 209 Map<String, String> componentsConfig) 210 { 211 Iterator<String> it = initialFeatures.keySet().iterator(); 212 while (it.hasNext()) 213 { 214 String id = it.next(); 215 Feature feature = initialFeatures.get(id); 216 217 if (incomingDeactivations.containsKey(id) && !incomingDeactivations.get(id).isEmpty()) 218 { 219 IncomingDeactivation deactivatingFeature = incomingDeactivations.get(id).iterator().next(); 220 _logger.debug("Removing feature {} deactivated by feature {}.", id, deactivatingFeature); 221 it.remove(); 222 inactiveFeatures.put(id, InactivityCause.DEACTIVATED); 223 continue; 224 } 225 226 Map<String, String> components = feature.getComponentsIds(); 227 for (String role : components.keySet()) 228 { 229 String componentId = components.get(role); 230 String selectedId = componentsConfig.get(role); 231 232 // remove the feature if the user asked for a specific id and the declared component has not that id 233 if (selectedId != null && !selectedId.equals(componentId)) 234 { 235 _logger.debug("Removing feature '{}' as it contains the component id '{}' for role '{}' but the user selected the id '{}' for that role.", id, componentId, role, selectedId); 236 it.remove(); 237 inactiveFeatures.put(id, InactivityCause.COMPONENT); 238 continue; 239 } 240 } 241 } 242 } 243 244 /** 245 * Remove the wrong points 246 * @param initialFeatures The initial features 247 * @param inactiveFeatures The inactive features 248 * @param extensionPoints The extension points 249 * @param errors The errors 250 */ 251 protected void removeWrongPointReferences( 252 Map<String, Feature> initialFeatures, 253 Map<String, InactivityCause> inactiveFeatures, 254 Map<String, ExtensionPointDefinition> extensionPoints, 255 Collection<PluginIssue> errors) 256 { 257 Set<String> ids = initialFeatures.keySet(); 258 Iterator<String> it = ids.iterator(); 259 while (it.hasNext()) 260 { 261 String id = it.next(); 262 Feature feature = initialFeatures.get(id); 263 Map<String, Collection<String>> extensionsIds = feature.getExtensionsIds(); 264 boolean hasBeenRemoved = false; 265 for (String point : extensionsIds.keySet()) 266 { 267 if (!extensionPoints.containsKey(point)) 268 { 269 String message = "In feature '" + id + "' an extension references the non-existing point '" + point + "'."; 270 _logger.error(message); 271 PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.INVALID_POINT, null, message); 272 errors.add(issue); 273 if (!hasBeenRemoved) 274 { 275 it.remove(); 276 inactiveFeatures.put(id, InactivityCause.INVALID_POINT); 277 hasBeenRemoved = true; 278 } 279 } 280 } 281 } 282 } 283 284 /** 285 * Process the outgoing dependencies 286 * @param initialFeatures The initial features 287 * @param inactiveFeatures The inactive features 288 * @param errors The errors 289 * @return the outgoing dependencies 290 */ 291 protected Map<String, Feature> processOutgoingDependencies( 292 Map<String, Feature> initialFeatures, 293 Map<String, InactivityCause> inactiveFeatures, 294 Collection<PluginIssue> errors) 295 { 296 // Check outgoing dependencies 297 boolean processDependencies = true; 298 while (processDependencies) 299 { 300 processDependencies = false; 301 302 Collection<String> ids = initialFeatures.keySet(); 303 Iterator<String> it = ids.iterator(); 304 while (it.hasNext()) 305 { 306 String id = it.next(); 307 if (_processOutgoingDependenciesForFeature(it, initialFeatures, inactiveFeatures, id)) 308 { 309 processDependencies = true; 310 } 311 } 312 } 313 314 // Reorder remaining features, respecting dependencies 315 Map<String, Feature> resultFeatures = new LinkedHashMap<>(); 316 317 for (String featureId : initialFeatures.keySet()) 318 { 319 _computeFeaturesDependencies(featureId, initialFeatures, resultFeatures, Collections.singletonList(featureId), errors); 320 } 321 322 return resultFeatures; 323 } 324 325 private boolean _processOutgoingDependenciesForFeature( 326 Iterator<String> initialFeatureIterator, 327 Map<String, Feature> initialFeatures, 328 Map<String, InactivityCause> inactiveFeatures, 329 String currentInitialFeatureId) 330 { 331 Feature currentInitialFeature = initialFeatures.get(currentInitialFeatureId); 332 Collection<String> dependencies = _correctedDependencies.getCorrectedDependencies(currentInitialFeature); 333 334 for (String dependency : dependencies) 335 { 336 if (!initialFeatures.containsKey(dependency)) 337 { 338 _logger.debug("The feature '{}' depends on '{}' which is not present. It will be ignored.", currentInitialFeatureId, dependency); 339 initialFeatureIterator.remove(); 340 inactiveFeatures.put(currentInitialFeatureId, InactivityCause.DEPENDENCY); 341 // processDependencies = true; 342 return true; // end iterate over dependencies (avoid a potential second it.remove() which will throw exception) and continue the while loop 343 } 344 } 345 346 return false; 347 } 348 349 private void _computeFeaturesDependencies( 350 String featureId, 351 Map<String, Feature> initialFeatures, 352 Map<String, Feature> resultFeatures, 353 List<String> involvedFeaturesInDependencyChain, 354 Collection<PluginIssue> errors) 355 { 356 Feature feature = Objects.requireNonNull(initialFeatures.get(featureId), String.format("Feature '%s' cannot be found", featureId)); 357 Collection<String> dependencies = _correctedDependencies.getCorrectedDependencies(feature); 358 359 for (String dependency : dependencies) 360 { 361 if (involvedFeaturesInDependencyChain.contains(dependency)) 362 { 363 String stringDependencyChain = Stream.concat(involvedFeaturesInDependencyChain.stream(), Stream.of(dependency)) 364 .collect(Collectors.joining("->")); 365 String message = "Circular dependency detected for feature: " + feature.getFeatureId() 366 + ". The dependency chain is: \n" 367 + stringDependencyChain; 368 _logger.error(message); 369 PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.CIRCULAR_DEPENDENCY, null, message); 370 errors.add(issue); 371 } 372 else if (!resultFeatures.containsKey(dependency)) 373 { 374 // do not process the feature if it has already been processed 375 List<String> dependencyChain = ListUtils.union(involvedFeaturesInDependencyChain, Collections.singletonList(dependency)); 376 _computeFeaturesDependencies(dependency, initialFeatures, resultFeatures, dependencyChain, errors); 377 } 378 } 379 380 resultFeatures.put(featureId, feature); 381 } 382 383 /** 384 * Compute incoming dependencies 385 * @param features The features 386 * @return The dependencies 387 */ 388 protected Map<String, Collection<String>> computeIncomingDependencies(Map<String, Feature> features) 389 { 390 Map<String, Collection<String>> incomingDependencies = new HashMap<>(); 391 for (String id : features.keySet()) 392 { 393 Feature feature = features.get(id); 394 Collection<String> dependencies = _correctedDependencies.getCorrectedDependencies(feature); 395 396 for (String dependency : dependencies) 397 { 398 Collection<String> deps = incomingDependencies.get(dependency); 399 if (deps == null) 400 { 401 deps = new ArrayList<>(); 402 incomingDependencies.put(dependency, deps); 403 } 404 405 deps.add(id); 406 } 407 } 408 409 return incomingDependencies; 410 } 411 412 /** 413 * Compute the outgoing dependencies 414 * @param features The features 415 * @return The dependencies 416 */ 417 protected Map<String, Collection<String>> computeOutgoingDependencies(Map<String, Feature> features) 418 { 419 Map<String, Collection<String>> outgoingDependencies = new HashMap<>(); 420 for (String id : features.keySet()) 421 { 422 Feature feature = features.get(id); 423 Collection<String> dependencies = _correctedDependencies.getCorrectedDependencies(feature); 424 outgoingDependencies.put(id, dependencies); 425 } 426 427 return outgoingDependencies; 428 } 429 430 /** 431 * Remove the unused features that were declared passive 432 * @param features The features 433 * @param inactiveFeatures The inactive features 434 * @param incomingDependencies The dependencies 435 */ 436 protected void removeUnusedPassiveFeatures( 437 Map<String, Feature> features, 438 Map<String, InactivityCause> inactiveFeatures, 439 Map<String, Collection<String>> incomingDependencies) 440 { 441 Set<String> ids = features.keySet(); 442 Iterator<String> it = ids.iterator(); 443 while (it.hasNext()) 444 { 445 String id = it.next(); 446 Feature feature = features.get(id); 447 448 if (feature.isPassive() && !incomingDependencies.containsKey(id)) 449 { 450 _logger.debug("Remove passive feature '{}'", id); 451 it.remove(); 452 inactiveFeatures.put(id, InactivityCause.PASSIVE); 453 } 454 } 455 } 456 457 /** 458 * Compute the extensions 459 * @param features The features 460 * @param errors The errors 461 * @return The extensions 462 */ 463 protected Map<String, Map<String, ExtensionDefinition>> computeExtensions( 464 Map<String, Feature> features, 465 Collection<PluginIssue> errors) 466 { 467 Map<String, Map<String, ExtensionDefinition>> extensionsDefinitions = new HashMap<>(); 468 for (Feature feature : features.values()) 469 { 470 // extensions 471 Map<String, Map<String, ExtensionDefinition>> extensionsConfs = feature.getExtensions(); 472 for (String point : extensionsConfs.keySet()) 473 { 474 Map<String, ExtensionDefinition> featureExtensions = extensionsConfs.get(point); 475 Map<String, ExtensionDefinition> globalExtensions = extensionsDefinitions.get(point); 476 if (globalExtensions == null) 477 { 478 globalExtensions = new LinkedHashMap<>(featureExtensions); 479 extensionsDefinitions.put(point, globalExtensions); 480 } 481 else 482 { 483 for (String id : featureExtensions.keySet()) 484 { 485 if (globalExtensions.containsKey(id)) 486 { 487 String message = "The extension '" + id + "' to point '" + point + "' is already defined in another feature."; 488 _logger.error(message); 489 PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.EXTENSION_ALREADY_EXIST, null, message); 490 errors.add(issue); 491 } 492 else 493 { 494 ExtensionDefinition definition = featureExtensions.get(id); 495 globalExtensions.put(id, definition); 496 } 497 } 498 } 499 } 500 } 501 502 return extensionsDefinitions; 503 } 504 505 /** 506 * compute the components 507 * @param features The features 508 * @param componentsConfig The component configurations 509 * @param errors The errors 510 * @return The components 511 */ 512 protected Map<String, ComponentDefinition> computeComponents( 513 Map<String, Feature> features, 514 Map<String, String> componentsConfig, 515 Collection<PluginIssue> errors) 516 { 517 Map<String, ComponentDefinition> components = new HashMap<>(); 518 519 for (Feature feature : features.values()) 520 { 521 // components 522 Map<String, ComponentDefinition> featureComponents = feature.getComponents(); 523 for (String role : featureComponents.keySet()) 524 { 525 ComponentDefinition definition = featureComponents.get(role); 526 ComponentDefinition globalDefinition = components.get(role); 527 if (globalDefinition == null) 528 { 529 components.put(role, definition); 530 } 531 else 532 { 533 String id = definition.getId(); 534 if (id.equals(globalDefinition.getId())) 535 { 536 String message = "The component for role '" + role + "' and id '" + id + "' is defined both in feature '" + definition.getPluginName() + PluginsManager.FEATURE_ID_SEPARATOR + definition.getFeatureName() + "' and in feature '" + globalDefinition.getPluginName() + PluginsManager.FEATURE_ID_SEPARATOR + globalDefinition.getFeatureName() + "'."; 537 _logger.error(message); 538 PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.COMPONENT_ALREADY_EXIST, null, message); 539 errors.add(issue); 540 } 541 else 542 { 543 String message = "The component for role '" + role + "' is defined with id '" + id + "' in the feature '" + definition.getPluginName() + PluginsManager.FEATURE_ID_SEPARATOR + definition.getFeatureName() + "' and with id '" + globalDefinition.getId() + "' in the feature '" + globalDefinition.getPluginName() + PluginsManager.FEATURE_ID_SEPARATOR + globalDefinition.getFeatureName() + "'. One of them should be chosen in the runtime.xml."; 544 _logger.error(message); 545 PluginIssue issue = new PluginIssue(feature.getPluginName(), feature.getFeatureName(), PluginIssueCode.COMPONENT_ALREADY_EXIST, null, message); 546 errors.add(issue); 547 } 548 } 549 } 550 } 551 552 // check that each component choosen in the runtime.xml is actually defined 553 for (String role : componentsConfig.keySet()) 554 { 555 String requiredId = componentsConfig.get(role); 556 ComponentDefinition definition = components.get(role); 557 558 if (definition == null || !definition.getId().equals(requiredId)) 559 { 560 // Due to preceding checks, the definition id should not be different than requiredId, but two checks are always better than one ... 561 String message = "The component for role '" + role + "' should point to id '" + requiredId + "' but no component match."; 562 _logger.error(message); 563 PluginIssue issue = new PluginIssue(null, null, PluginIssueCode.COMPONENT_NOT_DECLARED, null, message); 564 errors.add(issue); 565 } 566 } 567 568 return components; 569 } 570 571 @Override 572 public String fullDump(PluginsInformation pluginInfo) 573 { 574 return new LoadedFeaturesDump(this).fullDump(pluginInfo); 575 } 576}