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