001/* 002 * Copyright 2020 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.core.migration; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.Map.Entry; 025import java.util.Set; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.avalon.framework.thread.ThreadSafe; 035import org.apache.commons.lang3.StringUtils; 036import org.apache.commons.lang3.tuple.Pair; 037 038import org.ametys.core.ObservationConstants; 039import org.ametys.core.migration.action.Action; 040import org.ametys.core.migration.action.ActionExtensionPoint; 041import org.ametys.core.migration.action.data.ActionData; 042import org.ametys.core.migration.configuration.VersionConfiguration; 043import org.ametys.core.migration.handler.VersionHandler; 044import org.ametys.core.migration.handler.VersionHandlerExtensionPoint; 045import org.ametys.core.migration.version.Version; 046import org.ametys.core.observation.Event; 047import org.ametys.core.observation.ObservationManager; 048import org.ametys.runtime.plugin.ExtensionPoint; 049import org.ametys.runtime.plugin.component.AbstractLogEnabled; 050 051/** 052 * Migration Extension Point that will list all migration needed by the current state of the application 053 */ 054public class MigrationExtensionPoint extends AbstractLogEnabled implements ExtensionPoint<MigrationConfiguration>, ThreadSafe, Component, Serviceable 055{ 056 /** Avalon Role */ 057 public static final String ROLE = MigrationExtensionPoint.class.getName(); 058 059 private Map<String, MigrationConfiguration> _configurations = new HashMap<>(); 060 061 private Map<String, String> _expectedVersionsForExtensions = new HashMap<>(); 062 063 private VersionHandlerExtensionPoint _versionHandlerEP; 064 065 private ActionExtensionPoint _upgradeEP; 066 private ActionExtensionPoint _initializationEP; 067 068 private ObservationManager _observationManager; 069 070 public void service(ServiceManager smanager) throws ServiceException 071 { 072 _versionHandlerEP = (VersionHandlerExtensionPoint) smanager.lookup(VersionHandlerExtensionPoint.ROLE); 073 _upgradeEP = (ActionExtensionPoint) smanager.lookup(ActionExtensionPoint.ROLE_UPGRADE); 074 _initializationEP = (ActionExtensionPoint) smanager.lookup(ActionExtensionPoint.ROLE_INITIALIZATION); 075 076 if (smanager.hasService(ObservationManager.ROLE)) 077 { 078 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 079 } 080 } 081 082 public void addExtension(String id, String pluginName, String featureName, Configuration configuration) throws ConfigurationException 083 { 084 _configurations.put(id, new MigrationConfiguration(id, pluginName, featureName, configuration)); 085 } 086 087 public void initializeExtensions() throws Exception 088 { 089 // Nothing here 090 } 091 092 /** 093 * All migrations are parsed and executed one per one in the right order 094 */ 095 public void doMigration() 096 { 097 try 098 { 099 getLogger().info("Automatic migration start."); 100 101 List<ActionData> allUpgradeActions = new ArrayList<>(); 102 Map<Version, List<ActionData>> allInitActions = new HashMap<>(); 103 boolean versionDeterminationFailed = false; 104 Set<String> keySet = _configurations.keySet(); 105 for (String id : keySet) 106 { 107 MigrationConfiguration migrationConfiguration = _configurations.get(id); 108 109 try 110 { 111 String expectedVersion = _getHigherCurrentUpgradeVersionNumber(migrationConfiguration); 112 _expectedVersionsForExtensions.put(id, expectedVersion); 113 } 114 catch (ConfigurationException e) 115 { 116 throw new MigrationException("Impossible to read the upgrade configuration for component '" + id + "'", e); 117 } 118 119 try 120 { 121 Configuration configuration = migrationConfiguration.getConfiguration(); 122 123 List<Version> versions = new ArrayList<>(); 124 for (Configuration versionConfiguration : configuration.getChild("versions").getChildren("version")) 125 { 126 try 127 { 128 VersionHandler versionHandler = _getVersionHandler(versionConfiguration); 129 VersionConfiguration versionConfigurationObject = versionHandler.getConfiguration(id, versionConfiguration); 130 versions.addAll(versionHandler.getCurrentVersions(id, versionConfigurationObject)); 131 } 132 catch (NotMigrableInSafeModeException e) 133 { 134 getLogger().warn("The migration '{}' cannot be done in safe mode", id, e); 135 } 136 catch (MigrationException e) 137 { 138 versionDeterminationFailed = true; 139 getLogger().error("Error during version determination for component {}", id, e); 140 } 141 } 142 143 Map<Version, List<ActionData>> initActions = _getInitializationActions(versions, migrationConfiguration); 144 allInitActions.putAll(initActions); 145 146 List<ActionData> upgradeActions = _getUpgradeActions(versions, migrationConfiguration); 147 allUpgradeActions.addAll(upgradeActions); 148 } 149 catch (ConfigurationException | MigrationException e) 150 { 151 throw new MigrationException("Exception occured in migration " + id, e); 152 } 153 } 154 155 _applyInitializationActions(allInitActions); 156 _applyUpgradeActions(allUpgradeActions); 157 158 if (!versionDeterminationFailed) 159 { 160 _notifyEndOfMigration(); 161 162 // To free some memory space, not done on failing 163 _configurations = null; 164 } 165 166 getLogger().info("Automatic migration finished."); 167 } 168 catch (MigrationException e) 169 { 170 getLogger().error("Error during the automatic migration", e); 171 } 172 } 173 174 private VersionHandler _getVersionHandler(Configuration versionConfiguration) throws ConfigurationException 175 { 176 String versionHandlerType = versionConfiguration.getAttribute("type"); 177 178 VersionHandler extension = _versionHandlerEP.getExtension(versionHandlerType); 179 if (extension == null) 180 { 181 throw new ConfigurationException("A migration requests a versionHandler with id '" + versionHandlerType + "', which does not exists, this migration can not be done.", versionConfiguration); 182 } 183 return extension; 184 } 185 186 /** 187 * Notify with an {@link Event} the end of the migration. 188 */ 189 protected void _notifyEndOfMigration() 190 { 191 if (_observationManager != null) 192 { 193 _observationManager.notify(new Event(ObservationConstants.EVENT_MIGRATION_ENDED, null, Map.of())); 194 } 195 } 196 197 /** 198 * Execute all needed upgrades 199 * @param allInitializationActions list of needed upgrades 200 * @throws MigrationException Something went wrong 201 */ 202 protected void _applyInitializationActions(Map<Version, List<ActionData>> allInitializationActions) throws MigrationException 203 { 204 if (allInitializationActions.isEmpty()) 205 { 206 getLogger().debug("No initialization to do"); 207 } 208 else 209 { 210 for (Entry<Version, List<ActionData>> entry : allInitializationActions.entrySet()) 211 { 212 Version version = entry.getKey(); 213 List<ActionData> actions = entry.getValue(); 214 215 for (ActionData initializationAction : actions) 216 { 217 Action actionExtension = _initializationEP.getExtension(initializationAction.getType()); 218 getLogger().info("Run initialization : " + initializationAction.toString()); 219 actionExtension.doAction(initializationAction); 220 } 221 222 // The ID of the version had already be setted to the most recent action id 223 _upgradeVersion(version); 224 } 225 } 226 } 227 /** 228 * Execute all needed upgrades 229 * @param allActions list of needed upgrades 230 * @throws MigrationException Something went wrong 231 */ 232 protected void _applyUpgradeActions(List<ActionData> allActions) throws MigrationException 233 { 234 allActions.sort(Comparator.comparing(ActionData::getVersionNumber)); 235 if (allActions.isEmpty()) 236 { 237 getLogger().debug("No upgrade to do"); 238 } 239 else 240 { 241 for (ActionData action : allActions) 242 { 243 Action upgradeExtension = _upgradeEP.getExtension(action.getType()); 244 getLogger().info("Run upgrade : " + action.toString()); 245 upgradeExtension.doAction(action); 246 _upgradeVersion(action); 247 } 248 } 249 } 250 251 /** 252 * When an upgrade (or an initialization) have been done, we write the new version 253 * @param action upgrade/initialization executed 254 * @throws MigrationException Something went wrong 255 */ 256 protected void _upgradeVersion(ActionData action) throws MigrationException 257 { 258 Version newVersion = action.getVersion().copyFromActionData(action); 259 _upgradeVersion(newVersion); 260 } 261 262 /** 263 * When an upgrade (or an initialization) have been done, we write the new version 264 * @param newVersion upgrade/initialization executed 265 * @throws MigrationException Something went wrong 266 */ 267 protected void _upgradeVersion(Version newVersion) throws MigrationException 268 { 269 getLogger().debug("Update version info vor version : " + newVersion.toString()); 270 String versionHandlerId = newVersion.getVersionHandlerId(); 271 VersionHandler versionHandler = _versionHandlerEP.getExtension(versionHandlerId); 272 if (versionHandler == null) 273 { 274 throw new MigrationException("Version '" + newVersion.toString() + "' requires a versionHandler that does not exists: '" + versionHandlerId + "'."); 275 } 276 else 277 { 278 versionHandler.addVersion(newVersion); 279 } 280 } 281 282 /** 283 * Analyze the configuration to get a list of upgrades needed based on the current Versions 284 * @param currentVersions list of current versions 285 * @param migrationConfiguration configuration for this extension 286 * @return a list of actions to work on, or null if an error occured 287 * @throws MigrationException impossible to parse the configuration 288 */ 289 protected List<ActionData> _getUpgradeActions(List<Version> currentVersions, MigrationConfiguration migrationConfiguration) throws MigrationException 290 { 291 String componentId = migrationConfiguration.getId(); 292 try 293 { 294 Configuration[] upgradeList = migrationConfiguration.getConfiguration().getChild("upgrades").getChildren("upgrade"); 295 296 List<ActionData> result = new ArrayList<>(); 297 298 for (Version version : currentVersions) 299 { 300 if (version == null) 301 { 302 throw new MigrationException("The migration for '" + componentId + "' got a null Version."); 303 } 304 else if (version.getVersionNumber() != null) 305 { 306 getLogger().debug("Check the upgrades needed for version: {}", version.toString()); 307 String versionNumber = version.getVersionNumber(); 308 309 String higherCurrentUpgradeVersionNumber = _getHigherCurrentUpgradeVersionNumber(migrationConfiguration); 310 if (higherCurrentUpgradeVersionNumber.compareTo(versionNumber) < 0) 311 { 312 getLogger().warn("There is a version stored more recent that any version available in conf for version: {}", version.toString()); 313 } 314 315 List<ActionData> thisVersionUpgrades = new ArrayList<>(); 316 for (Configuration upgradeConf : upgradeList) 317 { 318 String upgradeVersionNumber = upgradeConf.getAttribute("versionNumber"); 319 String type = upgradeConf.getAttribute("type"); 320 String from = upgradeConf.getAttribute("from", null); 321 String comment = upgradeConf.getAttribute("comment", null); 322 323 if (comment == null) 324 { 325 comment = "Automatic Upgrade."; 326 } 327 else 328 { 329 comment = "Automatic Upgrade: " + comment; 330 } 331 332 Action actionExtension = _upgradeEP.getExtension(type); 333 334 if (actionExtension == null) 335 { 336 throw new ConfigurationException("The type '" + type + "' does not exist.", upgradeConf); 337 } 338 339 if (versionNumber.compareTo(upgradeVersionNumber) >= 0 // Obsolete migrations (migrating to older or current version) 340 || (from != null && versionNumber.compareTo(from) > 0)) // Future migration migrating from an older version 341 { 342 continue; 343 } 344 else 345 { 346 ActionData upgradeData = actionExtension.generateActionData(upgradeVersionNumber, version, comment, from, type, migrationConfiguration.getPluginName(), upgradeConf); 347 thisVersionUpgrades.add(upgradeData); 348 } 349 } 350 351 result.addAll(_removeDuplicatedActions(thisVersionUpgrades)); 352 } 353 } 354 355 return result; 356 } 357 catch (ConfigurationException e) 358 { 359 throw new MigrationException("Error while parsing configuration for component '" + componentId + "'", e); 360 } 361 } 362 363 /** 364 * Analyze the configuration to get a list of initializations needed based on the current Versions 365 * @param currentVersions list of current versions 366 * @param migrationConfiguration configuration for this extension 367 * @return a list of actions to work on, or null if an error occured 368 * @throws MigrationException impossible to parse the configuration 369 */ 370 protected Map<Version, List<ActionData>> _getInitializationActions(List<Version> currentVersions, MigrationConfiguration migrationConfiguration) throws MigrationException 371 { 372 String componentId = migrationConfiguration.getId(); 373 Map<Version, List<ActionData>> result = new HashMap<>(); 374 try 375 { 376 Configuration[] initializations = migrationConfiguration.getConfiguration().getChild("initializations").getChildren("initialization"); 377 378 String versionComment = migrationConfiguration.getConfiguration().getChild("initializations").getAttribute("comment", null); 379 380 if (versionComment == null) 381 { 382 versionComment = "Automatic Initialization."; 383 } 384 else 385 { 386 versionComment = "Automatic Initialization: " + versionComment; 387 } 388 389 for (Version version : currentVersions) 390 { 391 if (version == null) 392 { 393 throw new MigrationException("The migration for '" + componentId + "' got a null Version."); 394 } 395 else if (version.getVersionNumber() == null) 396 { 397 List<ActionData> thisVersionActions = new ArrayList<>(); 398 getLogger().debug("No version number, this is an initialization: {}", version.toString()); 399 400 String higherCurrentUpgradeVersionNumber = _getHigherCurrentUpgradeVersionNumber(migrationConfiguration); 401 version.setVersionNumber(higherCurrentUpgradeVersionNumber); 402 version.setComment(versionComment); 403 404 for (Configuration initialization : initializations) 405 { 406 String type = initialization.getAttribute("type"); 407 String actionComment = null; // No comment will be used here, only the version will have a comment in an initialization 408 409 Action actionExtension = _initializationEP.getExtension(type); 410 411 if (actionExtension == null) 412 { 413 throw new ConfigurationException("The type '" + type + "' does not exist.", initialization); 414 } 415 ActionData initializationData = actionExtension.generateActionData(higherCurrentUpgradeVersionNumber, version, actionComment, null, type, migrationConfiguration.getPluginName(), initialization); 416 thisVersionActions.add(initializationData); 417 } 418 419 result.put(version, thisVersionActions); 420 } 421 422 } 423 424 return result; 425 } 426 catch (ConfigurationException e) 427 { 428 throw new MigrationException("Error while parsing configuration for component '" + componentId + "'", e); 429 } 430 } 431 432 433 /** 434 * Returns the most recent version to apply (after an initialization) 435 * @param migrationConfiguration Configuration of the extension 436 * @return the id of the most current available version, or "0" if none available 437 * @throws ConfigurationException Sompthing wrong while reading the configuration 438 */ 439 protected String _getHigherCurrentUpgradeVersionNumber(MigrationConfiguration migrationConfiguration) throws ConfigurationException 440 { 441 Configuration[] upgradeList = migrationConfiguration.getConfiguration().getChild("upgrades").getChildren("upgrade"); 442 List<String> upgradeIds = new ArrayList<>(); 443 for (Configuration upgradeConf : upgradeList) 444 { 445 String id = upgradeConf.getAttribute("versionNumber"); 446 upgradeIds.add(id); 447 } 448 449 if (!upgradeIds.isEmpty()) 450 { 451 upgradeIds.sort(String.CASE_INSENSITIVE_ORDER); 452 return upgradeIds.get(upgradeIds.size() - 1); 453 } 454 else 455 { 456 return "0"; 457 } 458 } 459 460 /** 461 * Parse the list of upgrade to remove the group-migration 462 * @param actions list of upgrades to clean 463 * @return a list with only needed upgrades. 464 * @throws MigrationException list of actions incoherent 465 */ 466 protected List<ActionData> _removeDuplicatedActions(List<ActionData> actions) throws MigrationException 467 { 468 // First, sort the upgrades by if to have the right order 469 actions.sort( 470 Comparator.comparing(ActionData::getVersionNumber) 471 .thenComparing( 472 ActionData::getFrom, 473 Comparator.nullsFirst(Comparator.naturalOrder()) 474 ) 475 ); 476 477 // Create a list of pairs for each upgrade containing a "for" attribute to konw from which version to which version we should remove the lines 478 List<Pair<String, String>> fromTo = actions.stream() 479 .filter(u -> StringUtils.isNotBlank(u.getFrom())) 480 .map(u -> Pair.of(u.getFrom(), u.getVersionNumber())) 481 .collect(Collectors.toList()); 482 483 _checkFromUpgrades(actions, fromTo); 484 485 List<ActionData> result = actions; 486 487 // Invert the order so we will start to apply the most recent one, it may override other lines with a From inside 488 Collections.reverse(fromTo); 489 // For each pair, remove the right versions 490 for (Pair<String, String> pair : fromTo) 491 { 492 String from = pair.getLeft(); 493 String to = pair.getRight(); 494 495 result = result.stream() 496 .filter( 497 a -> a.getVersionNumber().compareTo(from) <= 0 498 || (to.equals(a.getVersionNumber()) && from.equals(a.getFrom())) 499 || a.getVersionNumber().compareTo(to) > 0 500 ) 501 .collect(Collectors.toList()); 502 } 503 504 return result; 505 } 506 507 /** 508 * Tests that in the list of fromTo, that each "fromTo" is linked to an action with the same id and without "from" 509 * @param actions list of actions 510 * @param fromTo list of actions overriding multiple actions 511 * @throws MigrationException there is an overriding version without a "simple" version with the same id 512 */ 513 protected void _checkFromUpgrades(List<ActionData> actions, List<Pair<String, String>> fromTo) throws MigrationException 514 { 515 for (Pair<String, String> pair : fromTo) 516 { 517 String from = pair.getLeft(); 518 String to = pair.getRight(); 519 boolean anyMatch = actions.stream().anyMatch(a -> a.getFrom() == null && to.equals(a.getVersionNumber())); 520 if (!anyMatch) 521 { 522 throw new MigrationException("The action from '" + from + "' to '" + to + "' does not contain a normal upgrade from '" + from + "'"); 523 } 524 } 525 } 526 527 private Map<String, MigrationConfiguration> _getConfigurationsForReading() 528 { 529 if (_configurations == null) 530 { 531 throw new UnsupportedOperationException("Configurations have been emptied from the extension point, this method can't be used for now."); 532 } 533 return _configurations; 534 } 535 536 public boolean hasExtension(String id) 537 { 538 return _getConfigurationsForReading().containsKey(id); 539 } 540 541 public MigrationConfiguration getExtension(String id) 542 { 543 return _getConfigurationsForReading().get(id); 544 } 545 546 public Set<String> getExtensionsIds() 547 { 548 return _getConfigurationsForReading().keySet(); 549 } 550 551 /** 552 * Get the expected version for a component. 553 * This is the latest upgrade number declared 554 * @param id the id of the component 555 * @return the expected version number for each version in this component 556 * @throws MigrationException Component not found in the calculated map of expected versions 557 */ 558 public String getExpectedVersionForComponent(String id) throws MigrationException 559 { 560 if (!_expectedVersionsForExtensions.containsKey(id)) 561 { 562 throw new MigrationException("Component '" + id + "' is not found in the calculated expected versions"); 563 } 564 return _expectedVersionsForExtensions.get(id); 565 } 566}