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