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