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