r1997 - trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration
Author: tchemit Date: 2010-06-08 23:51:15 +0200 (Tue, 08 Jun 2010) New Revision: 1997 Url: http://nuiton.org/repositories/revision/topia/1997 Log: Evolution #670: Rethink the migration service and deprecates old versions Added: trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationCallback.java trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationEngine.java Added: trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationCallback.java =================================================================== --- trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationCallback.java (rev 0) +++ trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationCallback.java 2010-06-08 21:51:15 UTC (rev 1997) @@ -0,0 +1,238 @@ +/* + * #%L + * ToPIA :: Service Migration + * + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2004 - 2010 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +package org.nuiton.topia.migration; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.jdbc.Work; +import org.nuiton.topia.TopiaContext; +import org.nuiton.topia.TopiaException; +import org.nuiton.topia.framework.TopiaContextImplementor; +import org.nuiton.util.StringUtil; +import org.nuiton.util.Version; + +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +import static org.nuiton.i18n.I18n._; + +/** + * Default migration call back to use. + * <p/> + * Replace deprecated implementation {@code ManualMigrationCallback}. + * + * @author tchemit <chemit@codelutin.com> + * @version $Id$ + * @since 2.4 + */ +public abstract class TopiaMigrationCallback { + + /** Logger */ + static private Log log = LogFactory.getLog(TopiaMigrationCallback.class); + + /** @return the available versions from the call back */ + public abstract Version[] getAvailableVersions(); + + /** @return the current application version (says the version to use) */ + public abstract Version getApplicationVersion(); + + /** + * Hook to ask user if migration can be performed. + * + * @param dbVersion the actual db version + * @param versions the versions to update + * @return {@code false} if migration is canceled, {@code true} otherwise. + */ + public abstract boolean askUser(Version dbVersion, + List<Version> versions); + + /** + * Tentative de migration depuis la version de la base version la version + * souhaitee. + * <p/> + * On applique toutes les migrations de version indiquee dans le parametre + * <code>version</code>. + * <p/> + * Pour chaque version, on cherche la methode migrateTo_XXX ou XXX est la + * version transforme en identifiant java via la methode + * {@link Version#getValidName()} et on l'execute. + * <p/> + * Note: pour chaque version a appliquer, on ouvre une nouvelle transaction. + * + * @param ctxt topia context de la transaction en cours + * @param dbVersion database version + * @param showSql drapeau pour afficher les requete sql + * @param showProgression drapeau pour afficher la progression + * @param versions all versions knwon by service @return migration a + * ggrement + * @return {@code true} si la migration est accepté, {@code false} autrement. + */ + public boolean doMigration(TopiaContext ctxt, + Version dbVersion, + boolean showSql, + boolean showProgression, + List<Version> versions) { + + boolean doMigrate = askUser(dbVersion, versions); + + if (doMigrate) { + TopiaContextImplementor tx; + + for (Version v : versions) { + // ouverture d'une connexion direct JDBC sur la base + try { + + tx = (TopiaContextImplementor) ctxt.beginTransaction(); + + try { + + Method m = getMigrationMethod(v); + + m.setAccessible(true); + + log.info(_("topia.migration.start.migrate", v)); + + if (log.isDebugEnabled()) { + log.debug("launch method " + m.getName()); + } + + m.invoke(this, tx, showSql, showProgression); + + // commit des modifs + tx.commitTransaction(); + + } catch (Exception eee) { + // en cas d'erreur + log.error("Migration impossible de la base", eee); + // rollback du travail en cours + tx.rollbackTransaction(); + } finally { + // close database connexion + if (tx != null) { + tx.closeContext(); + } + } + + } catch (Exception eee) { + log.error("Error lors de la tentative de migration", eee); + doMigrate = false; + } + } + } + return doMigrate; + } + + protected Method getMigrationMethod(Version version) throws NoSuchMethodException { + + String methodName = "migrateTo_" + version.getValidName(); + + Method m = getClass().getMethod(methodName, + TopiaContextImplementor.class, + boolean.class, + boolean.class + ); + return m; + } + + public void executeSQL(TopiaContextImplementor tx, String... sqls) + throws TopiaException { + executeSQL(tx, false, false, sqls); + } + + /** + * Executes the given {@code sqls} requests. + * + * @param tx the session + * @param showSql flag to see sql requests + * @param showProgression flag to see progession on console + * @param sqls requests to execute + * @throws TopiaException if any pb + * @since 2.3.0 + */ + public void executeSQL(TopiaContextImplementor tx, + final boolean showSql, + final boolean showProgression, + final String... sqls) throws TopiaException { + + if (log.isInfoEnabled()) { + + log.info(_("topia.migration.start.sqls", sqls.length)); + } + if (showSql) { + StringBuilder buffer = new StringBuilder(); + for (String s : sqls) { + buffer.append(s).append("\n"); + } + log.info("SQL TO EXECUTE :\n" + + "--------------------------------------------------------------------------------\n" + + "--------------------------------------------------------------------------------\n" + + buffer.toString() + + "--------------------------------------------------------------------------------\n" + + "--------------------------------------------------------------------------------\n" + ); + } + tx.getHibernate().doWork(new Work() { + + @Override + public void execute(Connection connection) throws SQLException { + int index = 0; + int max = sqls.length; + for (String sql : sqls) { + long t0 = System.nanoTime(); + if (log.isInfoEnabled()) { + String message = ""; + + if (showProgression) { + message = _("topia.migration.start.sql", ++index, max); + } + if (showSql) { + message += "\n" + sql; + } + if (showProgression || showSql) { + + log.info(message); + } + } + PreparedStatement sta = connection.prepareStatement(sql); + try { + sta.executeUpdate(); + } finally { + sta.close(); + } + if (log.isDebugEnabled()) { + String message; + message = _("topia.migration.end.sql", ++index, max, StringUtil.convertTime(System.nanoTime() - t0)); + log.debug(message); + } + } + } + }); + + } +} \ No newline at end of file Property changes on: trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationCallback.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision HeadURL Added: trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationEngine.java =================================================================== --- trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationEngine.java (rev 0) +++ trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationEngine.java 2010-06-08 21:51:15 UTC (rev 1997) @@ -0,0 +1,509 @@ +/* + * #%L + * ToPIA :: Service Migration + * + * $Id$ + * $HeadURL$ + * %% + * Copyright (C) 2004 - 2010 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ + +package org.nuiton.topia.migration; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.cfg.Configuration; +import org.hibernate.tool.hbm2ddl.SchemaExport; +import org.nuiton.topia.TopiaContext; +import org.nuiton.topia.TopiaException; +import org.nuiton.topia.TopiaRuntimeException; +import org.nuiton.topia.event.TopiaContextEvent; +import org.nuiton.topia.event.TopiaTransactionEvent; +import org.nuiton.topia.framework.TopiaContextImplementor; +import org.nuiton.topia.framework.TopiaUtil; +import org.nuiton.util.Version; +import org.nuiton.util.VersionUtil; +import org.nuiton.util.VersionUtil.VersionComparator; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.SortedSet; +import java.util.TreeSet; + +import static org.nuiton.i18n.I18n._; + +/** + * Le moteur de migration proposé par topia. Il est basé sur un {@link TopiaMigrationCallback} + * qui donne la version de l'application, les version de mises à jour disponibles. + * <p/> + * Le call back offre aussi les commandes sql à passer poru chaque version de mise à jour. + * <p/> + * TODO Finir cette documentation + * + * @author tchemit + * @version $Id$ + * @since 2.4 + */ +public class TopiaMigrationEngine implements TopiaMigrationService { + + /** logger */ + private final static Log log = LogFactory.getLog(TopiaMigrationEngine.class); + + /** L'unique handler a utiliser */ + static public final String MIGRATION_CALLBACK = "topia.service.migration.callback"; + + /** Un drapeau pour indiquer si on doit lancer le service au demarrage */ + static public final String MIGRATION_MIGRATE_ON_INIT = "topia.service.migration.no.migrate.on.init"; + + /** Pour afficher les requetes sql executees */ + static public final String MIGRATION_SHOW_SQL = "topia.service.migration.showSql"; + + /** Pour afficher la progression des requetes sql executees */ + static public final String MIGRATION_SHOW_PROGRESSION = "topia.service.migration.showProgression"; + + /** Configuration hibernate ne mappant que l'entite version (initialise en pre-init) */ + protected Configuration versionConfiguration; + + /** Un drapeau pour savoir si la table version existe en base (initialise en pre-init) */ + protected boolean versionTableExist; + + /** Version courante de la base (initialise en pre-init) */ + protected Version dbVersion; + + /** Drapeau pour savoir si la base est versionnée ou non */ + protected boolean dbNotVersioned; + + /** Un drapeau pour effectuer la migration au demarrage (initialise en pre-init) */ + protected boolean migrateOnInit; + + /** CallbackHandler list (initialise en pre-init) */ + protected TopiaMigrationCallback callback; + + /** topia root context (initialise en pre-init) */ + protected TopiaContextImplementor rootContext; + + /** Un drapeau pour savoir si le service a bien ete initialise (i.e a bien fini la methode preInit) */ + protected boolean init; + + /** Un drapeau pour afficher les requetes sql executees */ + protected boolean showSql; + + /** Un drapeau pour afficher la progression des requetes sql executees */ + protected boolean showProgression; + + @Override + public Class<?>[] getPersistenceClasses() { + return new Class<?>[]{TMSVersionImpl.class}; + } + + @Override + public String getServiceName() { + return SERVICE_NAME; + } + + protected String getSafeParameter(Properties config, String key) { + String value = config.getProperty(key, null); + if (StringUtils.isEmpty(value)) { + throw new IllegalStateException("'" + key + "' not set."); + } + return value; + } + + @Override + public boolean preInit(TopiaContextImplementor context) { + rootContext = context; + + Properties config = context.getConfig(); + + String callbackStr = getSafeParameter(config, MIGRATION_CALLBACK); + if (log.isDebugEnabled()) { + log.debug("Use callback - " + callbackStr); + } + + migrateOnInit = Boolean.valueOf(config.getProperty(MIGRATION_MIGRATE_ON_INIT, "true")); + if (log.isDebugEnabled()) { + log.debug("Migrate on init - " + migrateOnInit); + } + + showSql = Boolean.valueOf(config.getProperty(MIGRATION_SHOW_SQL, "false")); + if (log.isDebugEnabled()) { + log.debug("Show sql - " + showSql); + } + + showProgression = Boolean.valueOf(config.getProperty(MIGRATION_SHOW_PROGRESSION, "false")); + if (log.isDebugEnabled()) { + log.debug("Show progression - " + showProgression); + } + // enregistrement du callback + try { + Class<?> clazz = Class.forName(callbackStr); + callback = (TopiaMigrationCallback) clazz.newInstance(); + } catch (Exception e) { + log.error("Could not instanciate CallbackHandler [" + callbackStr + "]", e); + } + + // creation de la configuration hibernate ne concernant que l'entite Version + // afin de pouvoir creer la table via un schemaExport si necessaire + + versionConfiguration = new Configuration(); + + for (Class<?> clazz : getPersistenceClasses()) { + if (log.isDebugEnabled()) { + log.debug("addClass " + clazz); + } + versionConfiguration.addClass(clazz); + } + + Properties prop = new Properties(); + prop.putAll(versionConfiguration.getProperties()); + prop.putAll(config); + + versionConfiguration.setProperties(prop); + + init = true; + + // add topia context listener + context.addTopiaContextListener(this); + context.addTopiaTransactionVetoable(this); + + if (migrateOnInit) { + try { + + doMigrateSchema(); + + } catch (MigrationServiceException e) { + throw new TopiaRuntimeException("Can't migrate schema for reason " + e.getMessage(), e); + } + } else { + if (log.isDebugEnabled()) { + log.debug("migration service, skip migration on init"); + } + } + + return true; + } + + @Override + public boolean postInit(TopiaContextImplementor context) { + return true; + } + + @Override + public void preCreateSchema(TopiaContextEvent event) { + } + + @Override + public void preRestoreSchema(TopiaContextEvent event) { + } + + @Override + public void preUpdateSchema(TopiaContextEvent event) { + } + + @Override + public void postCreateSchema(TopiaContextEvent event) { + + if (log.isDebugEnabled()) { + log.debug("postCreateSchema event called : put version in database"); + } + + saveApplicationVersion(); + } + + @Override + public void postUpdateSchema(TopiaContextEvent event) { + if (log.isDebugEnabled()) { + log.debug("postUpdateSchema event called : put version in database"); + } + + saveApplicationVersion(); + } + + @Override + public void postRestoreSchema(TopiaContextEvent event) { + if (log.isDebugEnabled()) { + log.debug("postRestoreSchema event detected, redo, schema migration"); + } + try { + + doMigrateSchema(); + + } catch (Exception e) { + if (log.isErrorEnabled()) { + log.error("postRestoreSchema schema migration failed for reason " + e.getMessage(), e); + } + } + } + + @Override + public void beginTransaction(TopiaTransactionEvent event) { + + TopiaContextImplementor context = (TopiaContextImplementor) event.getSource(); + + // add topia context listener + context.addTopiaContextListener(this); + } + + public void doMigrateSchema() throws MigrationServiceException { + // migration + boolean complete = migrateSchema(); + if (!complete) { + if (log.isErrorEnabled()) { + log.error(_("topia.migration.migration.incomplete")); + } + throw new TopiaRuntimeException(_("topia.migration.migration.incomplete")); + } + } + + @Override + public boolean migrateSchema() throws MigrationServiceException { + + checkInit(); + + if (dbVersion == null || !migrateOnInit) { + + // no db version was setted or service was not init on int + // force detection of version to be safe + if (log.isDebugEnabled()) { + log.debug("Will detects db version..."); + } + detectDbVersion(); + } + + Version applicationVersion = callback.getApplicationVersion(); + + log.info(_("topia.migration.start.migration", applicationVersion.getVersion(), dbVersion.getVersion())); + + // tell if migration is needed + boolean bMigrationNeeded = false; + + if (log.isDebugEnabled()) { + log.debug("Migrate schema start version = " + dbVersion + + " _ not versioned = " + dbNotVersioned + + " _ TMSVersion exists = " + versionTableExist); + } + + if (versionTableExist && dbVersion.equals(applicationVersion)) { + log.info(_("topia.migration.skip.migration.db.is.up.to.date")); + // la base est a jour + return true; + } + + // Aucune version existante, la base de données est vierge + if (versionTableExist && dbNotVersioned && migrateOnInit) { + log.info(_("topia.migration.skip.migration.db.is.empty")); + // la base est vierge, aucune migration nécessaire + // mise à jour de la table tmsversion + saveApplicationVersion(); + return true; + } + + SortedSet<Version> allVersions = + new TreeSet<Version>(new VersionComparator()); + allVersions.addAll(Arrays.asList(callback.getAvailableVersions())); + log.info(_("topia.migration.available.versions", allVersions)); + + if (dbVersion.before(applicationVersion)) { + + // on filtre les versions a appliquer + List<Version> versionsToApply = + VersionUtil.filterVersions(allVersions, + dbVersion, + applicationVersion, + false, + true + ); + + if (versionsToApply.isEmpty()) { + log.info(_("topia.migration.skip.migration.no.version.to.apply")); + } else { + log.info(_("topia.migration.migrate.versions", versionsToApply)); + // ask handler for migration + bMigrationNeeded = callback.doMigration(rootContext, + dbVersion, + showSql, + showProgression, + versionsToApply); + + if (log.isDebugEnabled()) { + log.debug("Handler choose : " + bMigrationNeeded); + } + if (!bMigrationNeeded) { + // l'utilisateur a annule la migration + return false; + } + } + } + + // on sauvegarde la version si necessaire (base non versionnee ou migration realisee) + if (!versionTableExist || bMigrationNeeded) { + + if (log.isDebugEnabled()) { + log.debug("Set application version in database to " + applicationVersion); + } + + // put version in database and create table if required + saveApplicationVersion(); + } + + // return succes flag + // - no migration needed + // - or migration needed and accepted + return true; + } + + /** + * Enregistre la version donnee en base avec creation de la table + * si elle n'existe pas. + */ + public void saveApplicationVersion() { + + checkInit(); + + Version version = callback.getApplicationVersion(); + + if (log.isDebugEnabled()) { + log.debug("Save version = " + version + + " _ table exists = " + versionTableExist); + } + + if (dbVersion == null) { + detectDbVersion(); + } + + try { + + boolean createTable = !versionTableExist; + // update version even if database has not been migrated + // only case that database doesn't exist match this + if (createTable) { + // si la base n'etait pas versionnee, la table version n'existe pas + // creation + if (log.isDebugEnabled()) { + log.debug("Adding table to put version"); + } + + // creer le schema en base + // dans la configuration versionConfiguration, il n'y a que la table version + SchemaExport schemaExport = new SchemaExport(versionConfiguration); + schemaExport.create(log.isDebugEnabled(), true); + + if (log.isDebugEnabled()) { + log.debug("Table for " + TMSVersion.class.getSimpleName() + " created"); + } + + } + // Changement de la version en base + TopiaContext tx = rootContext.beginTransaction(); + try { + + TMSVersionDAO dao = MigrationServiceDAOHelper.getTMSVersionDAO(tx); + + //FIXME on supprime toues les versions precedentes ??? + //FIXME il serait mieux de conserver toutes les versions je pense... + //FIXME on pourrait conserver l'information sur les date de mise a jour + List<TMSVersion> toDelete = dao.findAll(); + for (TMSVersion v : toDelete) { + dao.delete(v); + } + + log.info(_("topia.migration.saving.db.version", version)); + dao.create(TMSVersion.VERSION, version.getVersion()); + + tx.commitTransaction(); + } catch (TopiaException e) { + if (tx != null) { + tx.rollbackTransaction(); + } + throw e; + } finally { + if (tx != null) { + tx.closeContext(); + } + } + } catch (TopiaException e) { + throw new TopiaRuntimeException(e); + } + + // on change les etats internes du service + // ainsi cela empechera le redeclanchement de la migration + // suite a une creation de schema + versionTableExist = true; + dbVersion = version; + } + + /** + * Recupere depuis la base les états internes du service : + * <p/> + * <ul> + * <li>versionTableExist</li> + * <li>dbVersion</li> + * </ul> + */ + protected void detectDbVersion() { + + // on detecte si la table de versionning existe + versionTableExist = TopiaUtil.isSchemaExist(versionConfiguration, TMSVersionImpl.class.getName()); + + if (log.isDebugEnabled()) { + log.debug("Table TMSVersion exist = " + versionTableExist); + } + // recuperation de la version de la base + Version v = null; + try { + if (versionTableExist) { + TopiaContext tx = null; + try { + tx = rootContext.beginTransaction(); + TMSVersionDAO dao = MigrationServiceDAOHelper.getTMSVersionDAO(tx); + List<TMSVersion> versionsInDB = dao.findAll(); + if (!versionsInDB.isEmpty()) { + v = VersionUtil.valueOf(versionsInDB.get(0).getVersion()); + } + } finally { + if (tx != null) { + tx.closeContext(); + } + } + } + } catch (TopiaException e) { + throw new TopiaRuntimeException("Can't obtain dbVersion for reason " + e.getMessage(), e); + } + + if (v == null) { + // la base dans ce cas n'est pas versionee. + // On dit que la version de la base est 0 + // et les schema de cette version 0 doivent + // etre detenu en local + v = Version.VZERO; + dbNotVersioned = true; + log.info(_("topia.migration.db.not.versionned")); + } else { + log.info(_("topia.migration.detected.db.version", v)); + } + dbVersion = v; + } + + protected void checkInit() { + if (!init) { + throw new IllegalStateException("le service n'est pas initialisé!"); + } + } +} \ No newline at end of file Property changes on: trunk/topia-service-migration/src/main/java/org/nuiton/topia/migration/TopiaMigrationEngine.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision HeadURL
participants (1)
-
tchemit@users.nuiton.org