001package systems.dmx.core.impl; 002 003import systems.dmx.core.service.Migration; 004import systems.dmx.core.service.ModelFactory; 005import systems.dmx.core.service.Plugin; 006import systems.dmx.core.util.JavaUtils; 007 008import org.codehaus.jettison.json.JSONArray; 009import org.codehaus.jettison.json.JSONException; 010import org.codehaus.jettison.json.JSONObject; 011import org.codehaus.jettison.json.JSONTokener; 012 013import java.io.InputStream; 014import java.io.IOException; 015import java.lang.reflect.Field; 016import java.util.Properties; 017import java.util.logging.Logger; 018 019 020 021class MigrationManager { 022 023 // ------------------------------------------------------------------------------------------------------- Constants 024 025 private static final String CORE_MIGRATIONS_PACKAGE = "systems.dmx.core.migrations"; 026 private static final int CORE_MODEL_VERSION = 7; 027 028 // ---------------------------------------------------------------------------------------------- Instance Variables 029 030 private CoreServiceImpl dmx; 031 private ModelFactory mf; 032 033 private enum MigrationRunMode { 034 CLEAN_INSTALL, UPDATE, ALWAYS 035 } 036 037 private Logger logger = Logger.getLogger(getClass().getName()); 038 039 // ---------------------------------------------------------------------------------------------------- Constructors 040 041 MigrationManager(CoreServiceImpl dmx) { 042 this.dmx = dmx; 043 this.mf = dmx.mf; 044 } 045 046 // ----------------------------------------------------------------------------------------- Package Private Methods 047 048 /** 049 * Determines the migrations to be run for the specified plugin, and runs them. 050 */ 051 void runPluginMigrations(PluginImpl plugin, boolean isCleanInstall) { 052 int installedModelVersion = plugin.getPluginTopic().getChildTopics().getTopic("dmx.core.plugin_migration_nr") 053 .getSimpleValue().intValue(); 054 int requiredModelVersion = Integer.parseInt(plugin.getConfigProperty("dmx.plugin.model_version", "0")); 055 int migrationsToRun = requiredModelVersion - installedModelVersion; 056 // 057 if (migrationsToRun == 0) { 058 logger.info("Running migrations for " + plugin + " SKIPPED -- installed model is up-to-date (version " + 059 installedModelVersion + ")"); 060 return; 061 } 062 // 063 logger.info("Running " + migrationsToRun + " migrations for " + plugin + " (installed model: version " + 064 installedModelVersion + ", required model: version " + requiredModelVersion + ")"); 065 for (int i = installedModelVersion + 1; i <= requiredModelVersion; i++) { 066 runMigration(i, plugin, isCleanInstall); 067 } 068 } 069 070 /** 071 * Determines the core migrations to be run, and runs them. 072 */ 073 void runCoreMigrations(boolean isCleanInstall) { 074 int installedModelVersion = dmx.pl.fetchMigrationNr(); 075 int requiredModelVersion = CORE_MODEL_VERSION; 076 int migrationsToRun = requiredModelVersion - installedModelVersion; 077 // 078 if (migrationsToRun == 0) { 079 logger.info("Running core migrations SKIPPED -- installed model is up-to-date (version " + 080 installedModelVersion + ")"); 081 return; 082 } 083 // 084 logger.info("Running " + migrationsToRun + " core migrations (installed model: version " + 085 installedModelVersion + ", required model: version " + requiredModelVersion + ")"); 086 for (int i = installedModelVersion + 1; i <= requiredModelVersion; i++) { 087 runMigration(i, null, isCleanInstall); // plugin=null 088 } 089 } 090 091 // ------------------------------------------------------------------------------------------------- Private Methods 092 093 /** 094 * Collects the info needed to run a migration, runs it, and then updates the version number in the DB. 095 * 096 * @param migrationNr Number of the migration to run. 097 * @param plugin The plugin that provides the migration to run. 098 * <code>null</code> for a core migration. 099 * @param isCleanInstall <code>true</code> if the migration is run as part of a clean install, 100 * <code>false</code> if the migration is run as part of an update. 101 */ 102 private void runMigration(int migrationNr, PluginImpl plugin, boolean isCleanInstall) { 103 MigrationInfo mi = null; 104 try { 105 // collect info 106 mi = new MigrationInfo(migrationNr, plugin); 107 if (!mi.success) { 108 throw mi.exception; // is not a runtime exception 109 } 110 mi.checkValidity(); 111 // 112 _runMigration(mi, isCleanInstall); 113 // 114 updateVersionNumber(mi); 115 } catch (Exception e) { 116 // Note: mi is instantiated for sure 117 throw new RuntimeException("Running " + mi.migrationInfo + " failed", e); 118 } 119 } 120 121 /** 122 * Runs a migration. 123 */ 124 private void _runMigration(MigrationInfo mi, boolean isCleanInstall) throws Exception { 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 readMigrationFile(mi.migrationIn, mi.migrationFile); 131 } else { 132 Migration migration = (Migration) mi.migrationClass.newInstance(); // throws InstantiationException, ... 133 injectServices(migration, mi.migrationInfo, mi.plugin); 134 migration.setCoreService(dmx); 135 logger.info("Running " + mi.migrationType + " migration class " + mi.migrationClassName); 136 migration.run(); 137 } 138 } else { 139 logger.info("Running " + mi.migrationInfo + " SKIPPED" + runInfo); 140 } 141 } 142 143 private void updateVersionNumber(MigrationInfo mi) { 144 try { 145 logger.info("Updating installed model: version " + mi.migrationNr); 146 if (mi.migrationType.equals("core")) { 147 dmx.pl.storeMigrationNr(mi.migrationNr); 148 } else { 149 mi.plugin.setMigrationNr(mi.migrationNr); 150 } 151 } catch (Exception e) { 152 throw new RuntimeException("Updating the model version number to " + mi.migrationNr + " failed", e); 153 } 154 } 155 156 // --- 157 158 private void injectServices(Migration migration, String migrationInfo, PluginImpl plugin) { 159 try { 160 for (Field field : PluginImpl.getInjectableFields(migration.getClass())) { 161 Class<?> serviceInterface = field.getType(); 162 Object service; 163 // 164 if (serviceInterface.getName().equals(plugin.getProvidedServiceInterface())) { 165 // the migration consumes the plugin's own service 166 service = plugin.getContext(); 167 } else { 168 service = plugin.getInjectedService(serviceInterface); 169 } 170 // 171 logger.info("Injecting service " + serviceInterface.getName() + " into " + migrationInfo); 172 field.set(migration, service); // throws IllegalAccessException 173 } 174 } catch (Exception e) { 175 throw new RuntimeException("Injecting services into " + migrationInfo + " failed", e); 176 } 177 } 178 179 // --- 180 181 /** 182 * Creates types and topics from a JSON formatted input stream. 183 * 184 * @param migrationFileName The origin migration file. Used for logging only. 185 */ 186 private void readMigrationFile(InputStream in, String migrationFileName) { 187 try { 188 logger.info("Reading migration file \"" + migrationFileName + "\""); 189 String fileContent = JavaUtils.readText(in); 190 // 191 Object value = new JSONTokener(fileContent).nextValue(); 192 if (value instanceof JSONObject) { 193 readEntities((JSONObject) value); 194 } else if (value instanceof JSONArray) { 195 readEntities((JSONArray) value); 196 } else { 197 throw new RuntimeException("Invalid file content"); 198 } 199 } catch (Exception e) { 200 throw new RuntimeException("Reading migration file \"" + migrationFileName + "\" failed", e); 201 } 202 } 203 204 private void readEntities(JSONArray entities) throws JSONException { 205 for (int i = 0; i < entities.length(); i++) { 206 readEntities(entities.getJSONObject(i)); 207 } 208 } 209 210 private void readEntities(JSONObject entities) throws JSONException { 211 JSONArray topicTypes = entities.optJSONArray("topic_types"); 212 if (topicTypes != null) { 213 createTopicTypes(topicTypes); 214 } 215 JSONArray assocTypes = entities.optJSONArray("assoc_types"); 216 if (assocTypes != null) { 217 createAssociationTypes(assocTypes); 218 } 219 JSONArray topics = entities.optJSONArray("topics"); 220 if (topics != null) { 221 createTopics(topics); 222 } 223 JSONArray assocs = entities.optJSONArray("associations"); 224 if (assocs != null) { 225 createAssociations(assocs); 226 } 227 } 228 229 private void createTopicTypes(JSONArray topicTypes) throws JSONException { 230 for (int i = 0; i < topicTypes.length(); i++) { 231 dmx.createTopicType(mf.newTopicTypeModel(topicTypes.getJSONObject(i))); 232 } 233 } 234 235 private void createAssociationTypes(JSONArray assocTypes) throws JSONException { 236 for (int i = 0; i < assocTypes.length(); i++) { 237 dmx.createAssociationType(mf.newAssociationTypeModel(assocTypes.getJSONObject(i))); 238 } 239 } 240 241 private void createTopics(JSONArray topics) throws JSONException { 242 for (int i = 0; i < topics.length(); i++) { 243 dmx.createTopic(mf.newTopicModel(topics.getJSONObject(i))); 244 } 245 } 246 247 private void createAssociations(JSONArray assocs) throws JSONException { 248 for (int i = 0; i < assocs.length(); i++) { 249 dmx.createAssociation(mf.newAssociationModel(assocs.getJSONObject(i))); 250 } 251 } 252 253 // ------------------------------------------------------------------------------------------------- Private Classes 254 255 /** 256 * Collects the info required to run a migration. 257 */ 258 private class MigrationInfo { 259 260 int migrationNr; 261 PluginImpl plugin; 262 // 263 String migrationType; // "core", "plugin" 264 String migrationInfo; // for logging 265 String runMode; // "CLEAN_INSTALL", "UPDATE", "ALWAYS" 266 // 267 boolean isDeclarative; 268 boolean isImperative; 269 // 270 String migrationFile; // for declarative migration 271 InputStream migrationIn; // for declarative migration 272 // 273 String migrationClassName; // for imperative migration 274 Class migrationClass; // for imperative migration 275 // 276 boolean success; // error occurred while construction? 277 Exception exception; // the error 278 279 MigrationInfo(int migrationNr, PluginImpl plugin) { 280 try { 281 this.migrationNr = migrationNr; 282 this.plugin = plugin; 283 String configFile = migrationConfigFile(); 284 InputStream configIn; 285 migrationFile = migrationFile(); 286 migrationType = plugin != null ? "plugin" : "core"; 287 // 288 if (migrationType.equals("core")) { 289 migrationInfo = "core migration " + migrationNr; 290 configIn = getClass().getResourceAsStream(configFile); 291 migrationIn = getClass().getResourceAsStream(migrationFile); 292 migrationClassName = coreMigrationClassName(); 293 migrationClass = loadClass(migrationClassName); 294 } else { 295 migrationInfo = "migration " + migrationNr + " of " + plugin; 296 configIn = getStaticResourceOrNull(configFile); 297 migrationIn = getStaticResourceOrNull(migrationFile); 298 migrationClassName = plugin.getMigrationClassName(migrationNr); 299 if (migrationClassName != null) { 300 migrationClass = plugin.loadClass(migrationClassName); 301 } 302 } 303 // 304 isDeclarative = migrationIn != null; 305 isImperative = migrationClass != null; 306 // 307 readMigrationConfigFile(configIn, configFile); 308 // 309 success = true; 310 } catch (Exception e) { 311 exception = e; 312 } 313 } 314 315 private void checkValidity() { 316 if (!isDeclarative && !isImperative) { 317 String message = "Neither a migration file (" + migrationFile + ") nor a migration class "; 318 if (migrationClassName != null) { 319 throw new RuntimeException(message + "(" + migrationClassName + ") is found"); 320 } else { 321 throw new RuntimeException(message + "is found. Note: a possible migration class can't be located" + 322 " (plugin package is unknown). Consider setting \"dmx.plugin.main_package\" in " + 323 "plugin.properties"); 324 } 325 } 326 if (isDeclarative && isImperative) { 327 throw new RuntimeException("Ambiguity: a migration file (" + migrationFile + ") AND a migration " + 328 "class (" + migrationClassName + ") are found. Consider using two different migration numbers."); 329 } 330 } 331 332 // --- 333 334 private void readMigrationConfigFile(InputStream in, String configFile) { 335 try { 336 Properties migrationConfig = new Properties(); 337 if (in != null) { 338 logger.info("Reading migration config file \"" + configFile + "\""); 339 migrationConfig.load(in); 340 } else { 341 logger.info("Reading migration config file \"" + configFile + "\" SKIPPED -- file does not exist"); 342 } 343 // 344 runMode = migrationConfig.getProperty("migrationRunMode", MigrationRunMode.ALWAYS.name()); 345 MigrationRunMode.valueOf(runMode); // check if value is valid 346 } catch (IllegalArgumentException e) { 347 throw new RuntimeException("Reading migration config file \"" + configFile + "\" failed: \"" + runMode + 348 "\" is an invalid value for \"migrationRunMode\"", e); 349 } catch (IOException e) { 350 throw new RuntimeException("Reading migration config file \"" + configFile + "\" failed", e); 351 } 352 } 353 354 // --- 355 356 private String migrationFile() { 357 return "/migrations/migration" + migrationNr + ".json"; 358 } 359 360 private String migrationConfigFile() { 361 return "/migrations/migration" + migrationNr + ".properties"; 362 } 363 364 private String coreMigrationClassName() { 365 return CORE_MIGRATIONS_PACKAGE + ".Migration" + migrationNr; 366 } 367 368 // --- Helper --- 369 370 private InputStream getStaticResourceOrNull(String resourceName) { 371 if (plugin.hasStaticResource(resourceName)) { 372 return plugin.getStaticResource(resourceName); 373 } else { 374 return null; 375 } 376 } 377 378 /** 379 * Uses the core bundle's class loader to load a class by name. 380 * 381 * @return the class, or <code>null</code> if the class is not found. 382 */ 383 private Class loadClass(String className) { 384 try { 385 return Class.forName(className); 386 } catch (ClassNotFoundException e) { 387 return null; 388 } 389 } 390 } 391}