001 package de.deepamehta.core.impl; 002 003 import de.deepamehta.core.service.Migration; 004 import de.deepamehta.core.util.DeepaMehtaUtils; 005 006 import java.io.InputStream; 007 import java.io.IOException; 008 import java.util.Properties; 009 import java.util.logging.Logger; 010 011 012 013 class MigrationManager { 014 015 // ------------------------------------------------------------------------------------------------------- Constants 016 017 private static final String CORE_MIGRATIONS_PACKAGE = "de.deepamehta.core.migrations"; 018 private static final int REQUIRED_CORE_MIGRATION = 3; 019 020 // ---------------------------------------------------------------------------------------------- Instance Variables 021 022 private EmbeddedService dms; 023 024 private enum MigrationRunMode { 025 CLEAN_INSTALL, UPDATE, ALWAYS 026 } 027 028 private Logger logger = Logger.getLogger(getClass().getName()); 029 030 // ---------------------------------------------------------------------------------------------------- Constructors 031 032 MigrationManager(EmbeddedService dms) { 033 this.dms = dms; 034 } 035 036 // ----------------------------------------------------------------------------------------- Package Private Methods 037 038 /** 039 * Determines the migrations to be run for the specified plugin and run them. 040 */ 041 void runPluginMigrations(PluginImpl plugin, boolean isCleanInstall) { 042 int migrationNr = plugin.getPluginTopic().getCompositeValue().getTopic("dm4.core.plugin_migration_nr") 043 .getSimpleValue().intValue(); 044 int requiredMigrationNr = Integer.parseInt(plugin.getConfigProperty("requiredPluginMigrationNr", "0")); 045 int migrationsToRun = requiredMigrationNr - migrationNr; 046 // 047 if (migrationsToRun == 0) { 048 logger.info("Running migrations for " + plugin + " ABORTED -- everything up-to-date (migrationNr=" + 049 migrationNr + ")"); 050 return; 051 } 052 // 053 logger.info("Running " + migrationsToRun + " migrations for " + plugin + " (migrationNr=" + migrationNr + 054 ", requiredMigrationNr=" + requiredMigrationNr + ")"); 055 for (int i = migrationNr + 1; i <= requiredMigrationNr; i++) { 056 runPluginMigration(plugin, i, isCleanInstall); 057 } 058 } 059 060 /** 061 * Determines the core migrations to be run and run them. 062 */ 063 void runCoreMigrations(boolean isCleanInstall) { 064 int migrationNr = dms.storageDecorator.fetchMigrationNr(); 065 int requiredMigrationNr = REQUIRED_CORE_MIGRATION; 066 int migrationsToRun = requiredMigrationNr - migrationNr; 067 // 068 if (migrationsToRun == 0) { 069 logger.info("Running core migrations ABORTED -- everything up-to-date (migrationNr=" + migrationNr + ")"); 070 return; 071 } 072 // 073 logger.info("Running " + migrationsToRun + " core migrations (migrationNr=" + migrationNr + 074 ", requiredMigrationNr=" + requiredMigrationNr + ")"); 075 for (int i = migrationNr + 1; i <= requiredMigrationNr; i++) { 076 runCoreMigration(i, isCleanInstall); 077 } 078 } 079 080 // ------------------------------------------------------------------------------------------------- Private Methods 081 082 private void runCoreMigration(int migrationNr, boolean isCleanInstall) { 083 runMigration(migrationNr, null, isCleanInstall); 084 dms.storageDecorator.storeMigrationNr(migrationNr); 085 } 086 087 private void runPluginMigration(PluginImpl plugin, int migrationNr, boolean isCleanInstall) { 088 runMigration(migrationNr, plugin, isCleanInstall); 089 plugin.setMigrationNr(migrationNr); 090 } 091 092 // --- 093 094 /** 095 * Runs a core migration or a plugin migration. 096 * 097 * @param migrationNr Number of the migration to run. 098 * @param plugin The plugin that provides the migration to run. 099 * <code>null</code> for a core migration. 100 * @param isCleanInstall <code>true</code> if the migration is run as part of a clean install, 101 * <code>false</code> if the migration is run as part of an update. 102 */ 103 private void runMigration(int migrationNr, PluginImpl plugin, boolean isCleanInstall) { 104 MigrationInfo mi = null; 105 try { 106 mi = new MigrationInfo(migrationNr, plugin); 107 if (!mi.success) { 108 throw mi.exception; 109 } 110 // error checks 111 if (!mi.isDeclarative && !mi.isImperative) { 112 String message = "Neither a migration file (" + mi.migrationFile + ") nor a migration class "; 113 if (mi.migrationClassName != null) { 114 throw new RuntimeException(message + "(" + mi.migrationClassName + ") is found"); 115 } else { 116 throw new RuntimeException(message + "is found. Note: a possible migration class can't be located" + 117 " (plugin package is unknown). Consider setting \"pluginPackage\" in plugin.properties"); 118 } 119 } 120 if (mi.isDeclarative && mi.isImperative) { 121 throw new RuntimeException("Ambiguity: a migration file (" + mi.migrationFile + ") AND a migration " + 122 "class (" + mi.migrationClassName + ") are found. Consider using two different migration numbers."); 123 } 124 // run migration 125 String runInfo = " (runMode=" + mi.runMode + ", isCleanInstall=" + isCleanInstall + ")"; 126 if (mi.runMode.equals(MigrationRunMode.CLEAN_INSTALL.name()) == isCleanInstall || 127 mi.runMode.equals(MigrationRunMode.ALWAYS.name())) { 128 logger.info("Running " + mi.migrationInfo + runInfo); 129 if (mi.isDeclarative) { 130 DeepaMehtaUtils.readMigrationFile(mi.migrationIn, mi.migrationFile, dms); 131 } else { 132 Migration migration = (Migration) mi.migrationClass.newInstance(); 133 logger.info("Running " + mi.migrationType + " migration class " + mi.migrationClassName); 134 migration.setCoreService(dms); 135 migration.run(); 136 } 137 logger.info("Completing " + mi.migrationInfo); 138 } else { 139 logger.info("Running " + mi.migrationInfo + " ABORTED" + runInfo); 140 } 141 logger.info("Updating migration number (" + migrationNr + ")"); 142 } catch (Exception e) { 143 throw new RuntimeException("Running " + mi.migrationInfo + " failed", e); 144 } 145 } 146 147 // ------------------------------------------------------------------------------------------------- Private Classes 148 149 /** 150 * Collects the info required to run a migration. 151 */ 152 private class MigrationInfo { 153 154 String migrationType; // "core", "plugin" 155 String migrationInfo; // for logging 156 String runMode; // "CLEAN_INSTALL", "UPDATE", "ALWAYS" 157 // 158 boolean isDeclarative; 159 boolean isImperative; 160 // 161 String migrationFile; // for declarative migration 162 InputStream migrationIn; // for declarative migration 163 // 164 String migrationClassName; // for imperative migration 165 Class migrationClass; // for imperative migration 166 // 167 boolean success; // error occurred? 168 Exception exception; // the error 169 170 MigrationInfo(int migrationNr, PluginImpl plugin) { 171 try { 172 String configFile = migrationConfigFile(migrationNr); 173 InputStream configIn; 174 migrationFile = migrationFile(migrationNr); 175 migrationType = plugin != null ? "plugin" : "core"; 176 // 177 if (migrationType.equals("core")) { 178 migrationInfo = "core migration " + migrationNr; 179 configIn = getClass().getResourceAsStream(configFile); 180 migrationIn = getClass().getResourceAsStream(migrationFile); 181 migrationClassName = coreMigrationClassName(migrationNr); 182 migrationClass = loadClass(migrationClassName); 183 } else { 184 migrationInfo = "migration " + migrationNr + " of " + plugin; 185 configIn = plugin.getResourceAsStream(configFile); 186 migrationIn = plugin.getResourceAsStream(migrationFile); 187 migrationClassName = plugin.getMigrationClassName(migrationNr); 188 if (migrationClassName != null) { 189 migrationClass = plugin.loadClass(migrationClassName); 190 } 191 } 192 // 193 isDeclarative = migrationIn != null; 194 isImperative = migrationClass != null; 195 // 196 readMigrationConfigFile(configIn, configFile); 197 // 198 success = true; 199 } catch (Exception e) { 200 exception = e; 201 } 202 } 203 204 // --- 205 206 private void readMigrationConfigFile(InputStream in, String configFile) { 207 try { 208 Properties migrationConfig = new Properties(); 209 if (in != null) { 210 logger.info("Reading migration config file \"" + configFile + "\""); 211 migrationConfig.load(in); 212 } else { 213 logger.info("Reading migration config file \"" + configFile + "\" ABORTED -- file does not exist"); 214 } 215 // 216 runMode = migrationConfig.getProperty("migrationRunMode", MigrationRunMode.ALWAYS.name()); 217 MigrationRunMode.valueOf(runMode); // check if value is valid 218 } catch (IllegalArgumentException e) { 219 throw new RuntimeException("Reading migration config file \"" + configFile + "\" failed: \"" + runMode + 220 "\" is an invalid value for \"migrationRunMode\"", e); 221 } catch (IOException e) { 222 throw new RuntimeException("Reading migration config file \"" + configFile + "\" failed", e); 223 } 224 } 225 226 // --- 227 228 private String migrationFile(int migrationNr) { 229 return "/migrations/migration" + migrationNr + ".json"; 230 } 231 232 private String migrationConfigFile(int migrationNr) { 233 return "/migrations/migration" + migrationNr + ".properties"; 234 } 235 236 private String coreMigrationClassName(int migrationNr) { 237 return CORE_MIGRATIONS_PACKAGE + ".Migration" + migrationNr; 238 } 239 240 // --- Generic Utilities --- 241 242 /** 243 * Uses the core bundle's class loader to load a class by name. 244 * 245 * @return the class, or <code>null</code> if the class is not found. 246 */ 247 private Class loadClass(String className) { 248 try { 249 return Class.forName(className); 250 } catch (ClassNotFoundException e) { 251 return null; 252 } 253 } 254 } 255 }