com.tacitknowledge.util.migration.MigrationProcess Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of autopatch Show documentation
Show all versions of autopatch Show documentation
An automated Java patching system
The newest version!
/* Copyright 2004 Tacit Knowledge * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tacitknowledge.util.migration; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.util.*; /** * Discovers and executes a sequence of system patches. Patches take the form of *where rollbacks would occer * @return the count of tasks which would rollback */ private int rollbackDryRun(ListMigrationTaskinstances, each of which performs an atomic * migration or patch transaction.MigrationTasks are executed * sequentially based on the result ofMigrationTask.getOrder. * No two tasks can return the same result forgetOrder, and * this class will throw aMigrationExceptionshould such a * situation occur. * * One useful pre-definedMigrationTaskis *SqlScriptMigrationTask, which wraps a .SQL file and executes * all statements inside it. Any file in the migration task search path that * matches the pattern "^patch(\\d++)(?!-rollback)_?(.+)?\\.sql" will be wrapped with the *SqlScriptMigrationTask. Also, you rollback scripts are specified by * matching the pattern "^patch(\\d+)-rollback_(.+)\\.sql." The rollback scripts * are meant to reverse a patch and will reduce the level of the system to the * previous patch level. The execution order for these tasks * is defined by the number immediately following the "patch" part of the SQL * script file name. * * Example: * ** // Find the patches * migrationRunner.addResourcePackage("com.example.myapp.migration"); * migrationRunner.addResourceDirectory("db/sql"); * * try * { * <i>... figure out the current patch level...</i> * migrationRunner.doMigration(currentLevel, context); * <i>... update patch level</i> * <i>... commit MigrationContext ...</i> * } * catch (MigrationException e) * { * <i>... rollback MigrationContext ...</i> * } ** * @author Scott Askew ([email protected]) * @author Hemri Herrera ([email protected]) * @author Ulises Pulido ([email protected]) */ public class MigrationProcess { /** * Class logger */ private static Log log = LogFactory.getLog(MigrationProcess.class); /** * The list of package names containing theMigrationTasks * and SQL scripts to execute as patches */ private List patchResourcePackages = new ArrayList(); /** * The list of package names containingMigrationTasks and * SQL scripts to execute after patch execution */ private List postPatchResourcePackages = new ArrayList(); /** * Migration task providers */ private ListmigrationTaskSources = new ArrayList (); /** * Used to broadcast migration task notifications */ private MigrationBroadcaster broadcaster = null; /** * Used to broadcast rollback task notifications */ private RollbackBroadcaster rollbackBroadcaster = new RollbackBroadcaster(); /** * Holds the strategy used to work with the different patches */ private MigrationRunnerStrategy migrationRunnerStrategy = null; /** * Whether we actually want to apply patches, or just look */ private boolean readOnly = false; /** * Creates a new Migrationinstance. */ public MigrationProcess() { addMigrationTaskSource(new ClassMigrationTaskSource()); setMigrationBroadcaster(new MigrationBroadcaster()); } public void setMigrationRunnerStrategy(MigrationRunnerStrategy migrationRunnerStrategy) { this.migrationRunnerStrategy = migrationRunnerStrategy; } /** * Sets theMigrationBroadcasterfor the current instance. * * @param migrationBroadcaster with theMigrationBroadcasterto be set */ protected void setMigrationBroadcaster(MigrationBroadcaster migrationBroadcaster) { this.broadcaster = migrationBroadcaster; } /** * Adds the given package to the migration task search path. * * @param packageName the name of the package to add to the search path */ public void addPatchResourcePackage(String packageName) { patchResourcePackages.add(packageName); } /** * Adds the given classpath-relative directory to the migration task search * path. * * @param dir the name of the directory to add to the post-patch search path */ public void addPatchResourceDirectory(String dir) { // Make the path package-name-like so that // ClassLoader.getResourceAsStream // will work correctly String packageName = dir.replace('/', '.').replace('\\', '.'); addPatchResourcePackage(packageName); } /** * Adds the given package to the post-patch migration task search path. * * @param packageName the name of the package to add to the search path */ public void addPostPatchResourcePackage(String packageName) { postPatchResourcePackages.add(packageName); } /** * Adds the given classpath-relative directory to the post-patch migration * task search path. * * @param dir the name of the directory to add to the post-patch search path */ public void addPostPatchResourceDirectory(String dir) { // Make the path package-name-like so that // ClassLoader.getResourceAsStream // will work correctly String packageName = dir.replace('/', '.').replace('\\', '.'); addPostPatchResourcePackage(packageName); } /** * Adds aMigrationTaskSourceto the list of sources that * provide this instance withMigrationTasks. * * @param source theMigrationTaskSourceto add; may not be *null*/ public void addMigrationTaskSource(MigrationTaskSource source) { if (source == null) { throw new IllegalArgumentException("source cannot be null."); } else { migrationTaskSources.add(source); } } /** * Applies rollbacks to move the patch level from the current level to the * rollback level. * * @param currentPatchInfoStore * @param rollbackLevels an integer indicating the level the desired patch level * @param context information and resources that are available to the migration tasks * @return the count of patches which were rolled back * @throws MigrationException */ public int doRollbacks(PatchInfoStore currentPatchInfoStore, int[] rollbackLevels, MigrationContext context, boolean forceRollback) throws MigrationException { log.trace("Starting doRollbacks"); ListallTasks = getMigrationTasks(); validateTasks(allTasks); List rollbackCandidates = getMigrationRunnerStrategy() .getRollbackCandidates(allTasks, rollbackLevels, currentPatchInfoStore); boolean isPatchSetRollbackable = isPatchSetRollbackable(rollbackCandidates); rollbackDryRun(rollbackCandidates, context); if (isPatchSetRollbackable || forceRollback) { // See if we should execute if (isReadOnly()) { throw new MigrationException("Unapplied rollbacks exist, but read-only flag is set"); } // the list of patches is rollbackable now actually perform the // rollback log.info("A total of " + rollbackCandidates.size() + " rollbacks will execute."); for (MigrationTask rollbackTask : rollbackCandidates) { RollbackableMigrationTask task = (RollbackableMigrationTask) rollbackTask; log.info("Will rollback patch task '" + getTaskLabel(task) + "'"); log.debug("Task will rollback in context '" + context + "'"); applyRollback(context, task, true); } } else { // Can I list the tasks which are not rollbackable? log.info("Could not complete rollback because one or more of the tasks " + "is not rollbackable.The system is still at patch level " + currentPatchInfoStore + "."); } List rollbacksNotApplied = getMigrationRunnerStrategy() .getRollbackCandidates(rollbackCandidates, rollbackLevels, currentPatchInfoStore); if (rollbacksNotApplied.isEmpty()) { log.info("Rollback complete. The system is now at the desired patch level."); } else { log.info("The system was not able to rollback the patches."); } log.trace("Ending doRollbacks"); return rollbackCandidates.size() - rollbacksNotApplied.size(); } /** * Helper method to determine if the set of migration tasks is rollbackable * * @param migrations a Listof MigrationTasks * @return a boolean value indicating if all of the tasks can be rolledback */ protected boolean isPatchSetRollbackable(List migrations) { boolean isRollbackable = true; for (Iterator i = migrations.iterator(); i.hasNext() && isRollbackable;) { // for each migration check if the task is able to be rolledback // think about type RollbackableMigrationTask task = null; try { task = (RollbackableMigrationTask) i.next(); } catch (ClassCastException cce) { isRollbackable = false; log.info("The task " + task.getName() + " is not rollbackable."); } if (!task.isRollbackSupported()) { isRollbackable = false; log.info("The task " + task.getName() + " is not rollbackable."); } } return isRollbackable; } /** * Applies necessary patches to the system. * * @param patchInfoStore used to execute migrations * @param context information and resources that are available to the migration tasks * @return the number ofMigrationTasks that have executed * @throws MigrationException if a migration fails */ public int doMigrations(PatchInfoStore patchInfoStore, MigrationContext context) throws MigrationException { log.trace("Starting doMigrations"); Listmigrations = getMigrationTasks(); validateTasks(migrations); Collections.sort(migrations); int taskCount = dryRun(patchInfoStore, context, migrations); // See if we should execute if (isReadOnly()) { if (taskCount > 0) { throw new MigrationException("Unapplied patches exist, but read-only flag is set"); } log.info("In read-only mode - skipping patch application"); return 0; } // Now apply them taskCount = 0; for (MigrationTask task : migrations) { if (migrationRunnerStrategy.shouldMigrationRun(task.getLevel(), patchInfoStore)) { applyPatch(context, task, true); taskCount++; } } if (taskCount > 0) { log.info("Patching complete (" + taskCount + " patch tasks executed)"); } else { log.info("System up-to-date. No patch tasks executed."); } return taskCount; } /** * Performs a dry run of rollbacks. This method determines which tasks will rollback * and logs this information. * * @param migrations a Listof migrations * @param context theMigrationContext migrations, MigrationContext context) { int taskCount = 0; // Roll through once, just printing out what we'll do for (MigrationTask migrationTask : migrations) { RollbackableMigrationTask task = (RollbackableMigrationTask) migrationTask; log.info("Will execute rollback for task '" + getTaskLabel(task) + "'"); log.debug("Task will execute in context '" + context + "'"); taskCount++; } if (taskCount > 0) { log.info("A total of " + taskCount + " patch tasks will rollback."); } else { log.info("System up-to-date. No patch tasks will execute."); } return taskCount; } /** * Run post-migration tasks * * @param context the context to use for the post-patch migrations * @return the number of MigrationTasks that executed * @throws MigrationException if a post-patch task fails */ public int doPostPatchMigrations(MigrationContext context) throws MigrationException { log.info("Running post-patch tasks..."); List postMigrationTasks = getPostPatchMigrationTasks(); validateTasks(postMigrationTasks); Collections.sort(postMigrationTasks); if (postMigrationTasks.size() == 0) { log.info("No post-patch tasks found."); return 0; } // Roll through once, just printing out what we'll do int taskCount = 0; for (Iterator i = postMigrationTasks.iterator(); i.hasNext(); taskCount++) { MigrationTask task = (MigrationTask) i.next(); log.info("Will execute post-patch task '" + getTaskLabel(task) + "'"); } log.info("A total of " + taskCount + " post-patch tasks will execute."); // See if we should execute // FIXME test read-only mode with no patches skipping post-patch tasks if (isReadOnly()) { log.info("In read-only mode - skipping post-patch task execution"); return 0; } // Now execute them taskCount = 0; for (Iterator i = postMigrationTasks.iterator(); i.hasNext(); taskCount++) { MigrationTask task = (MigrationTask) i.next(); applyPatch(context, task, false); } log.info("Post-patch tasks complete (" + taskCount + " tasks executed)"); return taskCount; } /** * This method applies a single Rollback to the system. * * @param context theMigrationContextin which the task is rolled back * @param task theRollbackableMigrationTaskwhich is to be rolled back * @param broadcast a boolean indicating if the execution of this rollback should be broadcast to listeners * @throws MigrationException is thrown in case of encountering an exception */ public void applyRollback(MigrationContext context, RollbackableMigrationTask task, boolean broadcast) throws MigrationException { String label = getTaskLabel(task); int rollbackLevel = task.getLevel().intValue(); if (broadcast) { rollbackBroadcaster.notifyListeners(task, context, MigrationBroadcaster.TASK_START, rollbackLevel); log .debug("broadcaster has " + rollbackBroadcaster.getListeners().size() + " listeners"); } log.info("Executing patch task \"" + label + "\"..."); try { long startTime = System.currentTimeMillis(); task.down(context); long duration = System.currentTimeMillis() - startTime; log.info("Finished patch task \"" + label + "\" (" + duration + " millis.)"); if (broadcast) { rollbackBroadcaster.notifyListeners(task, context, MigrationBroadcaster.TASK_SUCCESS, rollbackLevel); } context.commit(); } catch (MigrationException e) { if (broadcast) { rollbackBroadcaster.notifyListeners(task, context, e, MigrationBroadcaster.TASK_FAILED, rollbackLevel); } try { context.rollback(); log.info("Patch task failed; rollback successful"); } catch (MigrationException me) { log.info("Patch task failed; COULD NOT ROLL BACK TRANSACTION", me); } throw e; } } /** * Apply a single patch * * @param context the context the patch will need during application * @param task the application task to carry out * @param broadcast whether to broadcast to listeners that the patch applied * @throws MigrationException if the patch application fails */ public void applyPatch(MigrationContext context, MigrationTask task, boolean broadcast) throws MigrationException { String label = getTaskLabel(task); if (broadcast) { broadcaster.notifyListeners(task, context, MigrationBroadcaster.TASK_START); log.debug("broadcaster has " + broadcaster.getListeners().size() + " listeners"); } log.info("Executing patch task \"" + label + "\"..."); try { long startTime = System.currentTimeMillis(); task.migrate(context); long duration = System.currentTimeMillis() - startTime; log.info("Finished patch task \"" + label + "\" (" + duration + " millis.)"); if (broadcast) { broadcaster.notifyListeners(task, context, MigrationBroadcaster.TASK_SUCCESS); } context.commit(); } catch (MigrationException e) { if (broadcast) { broadcaster.notifyListeners(task, context, e, MigrationBroadcaster.TASK_FAILED); } try { context.rollback(); log.info("Patch task failed; rollback successful"); } catch (MigrationException me) { log.info("Patch task failed; COULD NOT ROLL BACK TRANSACTION", me); } throw e; } } /** * Returns a list of all migration tasks, regardless of patch level. * * @return a list of all migration tasks * @throws MigrationException if one or more migration tasks could not be created */ public ListgetMigrationTasks() throws MigrationException { return getTasksFromPackages(patchResourcePackages); } /** * Returns a list of all post-patch migration tasks * * @return a list of all post-patch migration tasks * @throws MigrationException if one or more post-patch migration tasks could not be created */ public List getPostPatchMigrationTasks() throws MigrationException { return getTasksFromPackages(postPatchResourcePackages); } /** * Instantiate all the MigrationTask objects in the given resource packages * * @param resourcePackages a List of Strings specifying package names to look for tasks in * @return List of MigrationTask objects instantiated from the given packages * @throws MigrationException if one or more post-patch migration tasks could not be * created */ private List getTasksFromPackages(List resourcePackages) throws MigrationException { List tasks = new ArrayList(); for (String packageName : resourcePackages) { log.debug("Searching for patch tasks in package " + packageName); for (MigrationTaskSource source : migrationTaskSources) { List sourceTasks = source.getMigrationTasks(packageName); if (sourceTasks.size() > 0) { log.debug("Source [" + source + "] found " + sourceTasks.size() + " patch tasks: " + sourceTasks); } else { log.debug("Source [" + source + "] returned 0 patch tasks."); } tasks.addAll(sourceTasks); } } // Its difficult to tell what's going on when you don't see any patches. // This will help people realize they don't have patches, and perhaps // help them discover why. if (tasks.size() == 0) { log.info("No patch tasks were discovered in your classpath. " + "Run with DEBUG logging enabled for patch task search details."); } return tasks; } /** * Returns the patch level which is previous to the current level * * @param currentLevel the current patch level * @return the level of the patch that was applied previous to the current patch l * @throws MigrationException if there is an error retrieving the previous patch level */ public int getPreviousPatchLevel(int currentLevel) throws MigrationException { boolean isCurrentPatchFound = false; List tasks = getMigrationTasks(); int previousTaskLevel = 0; Collections.sort(tasks); for (ListIterator patchIterator = tasks.listIterator(); patchIterator.hasNext() && !isCurrentPatchFound;) { int previousIndex = patchIterator.previousIndex(); //get the current patch MigrationTask task = (MigrationTask) patchIterator.next(); if (currentLevel == task.getLevel().intValue()) { //the current patch level is found isCurrentPatchFound = true; if (previousIndex != -1) { MigrationTask previousTask = (MigrationTask) tasks.get(previousIndex); previousTaskLevel = previousTask.getLevel().intValue(); } } } return previousTaskLevel; } /** * Returns the number to use when creating the next patch. * * @return the number to use when creating the next patch * @throws MigrationException if the existing tasks are invalid */ public int getNextPatchLevel() throws MigrationException { List tasks = getMigrationTasks(); if (tasks.size() == 0) { return 1; } Collections.sort(tasks); validateTasks(tasks); MigrationTask lastTask = (MigrationTask) tasks.get(tasks.size() - 1); return lastTask.getLevel().intValue() + 1; } /** * Registers the given MigrationListeneras being interested * in migration task events. * * @param listener the listener to add; may not benull*/ public void addListener(MigrationListener listener) { broadcaster.addListener(listener); if (listener instanceof RollbackListener) rollbackBroadcaster.addListener((RollbackListener) listener); } /** * Removes the givenMigrationListenerfrom the list of * listeners associated with thisMigrationinstance. * * @param listener the listener to add; may not benull* @returntrueif the listener was located and removed, * otherwisefalse. */ public boolean removeListener(MigrationListener listener) { return broadcaster.removeListener(listener); } /** * Get all of the MigrationListeners * * @return List of MigrationListeners */ public List getListeners() { return broadcaster.getListeners(); } /** * Returns a user-friendly label for the specified task. * * @param task the task to create a label for * @return a user-friendly label for the specified task */ protected String getTaskLabel(MigrationTask task) { return task.getName() + " [" + task.getClass().getName() + "]"; } /** * Ensures that no twoMigrationTaskshave the same ordering. * * @param migrations the list of defined migration tasks * @throws MigrationException if the migration tasks are not correctly defined */ public void validateTasks(List migrations) throws MigrationException { Map usedOrderNumbers = new HashMap(); for (Iterator i = migrations.iterator(); i.hasNext();) { MigrationTask task = (MigrationTask) i.next(); Integer level = task.getLevel(); if (level == null) { throw new MigrationException("Patch task '" + getTaskLabel(task) + "' does not have a patch level defined."); } if (usedOrderNumbers.containsKey(level)) { MigrationTask otherTask = (MigrationTask) usedOrderNumbers.get(level); throw new MigrationException("Patch task " + getTaskLabel(task) + " has a conflicting patch level with " + getTaskLabel(otherTask) + "; both are configured for patch level " + level); } usedOrderNumbers.put(level, task); } } /** * See if we are actually applying patches, or if it is just readonly * * @return boolean true if we will skip application */ public boolean isReadOnly() { return readOnly; } /** * Set whether or not to actually apply patches * * @param readOnly boolean true if we should skip application */ public void setReadOnly(boolean readOnly) { this.readOnly = readOnly; } /** * Registers the givenMigrationListenersas being interested * in migration task events. * * @param listeners the listeners to add; */ public void addListeners(List listeners) { for (Iterator it = listeners.iterator(); it.hasNext();) { MigrationListener listener = (MigrationListener) it.next(); broadcaster.addListener(listener); if (listener instanceof RollbackListener) rollbackBroadcaster.addListener((RollbackListener) listener); } } public int dryRun(PatchInfoStore patchInfoStore, MigrationContext migrationContext, List migrations) throws MigrationException { int taskCount = 0; // Roll through once, just printing out what we'll do for (Iterator i = migrations.iterator(); i.hasNext();) { MigrationTask task = (MigrationTask) i.next(); if (migrationRunnerStrategy .shouldMigrationRun(task.getLevel().intValue(), patchInfoStore)) { log.info("Will execute patch task '" + getTaskLabel(task) + "'"); log.debug("Task will execute in context '" + migrationContext + "'"); taskCount++; } } if (taskCount > 0) { log.info("A total of " + taskCount + " patch tasks will execute."); } else { log.info("System up-to-date. No patch tasks will execute."); } return taskCount; } public MigrationRunnerStrategy getMigrationRunnerStrategy() { return migrationRunnerStrategy; } }
© 2015 - 2025 Weber Informatics LLC | Privacy Policy