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