001package de.deepamehta.core.impl; 002 003import de.deepamehta.core.service.Migration; 004import de.deepamehta.core.service.ModelFactory; 005import de.deepamehta.core.service.Plugin; 006import de.deepamehta.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 = "de.deepamehta.core.migrations"; 026 private static final int CORE_MODEL_VERSION = 7; 027 028 // ---------------------------------------------------------------------------------------------- Instance Variables 029 030 private CoreServiceImpl dm4; 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 dm4) { 042 this.dm4 = dm4; 043 this.mf = dm4.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("dm4.core.plugin_migration_nr") 053 .getSimpleValue().intValue(); 054 int requiredModelVersion = Integer.parseInt(plugin.getConfigProperty("dm4.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 runPluginMigration(plugin, i, 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 = dm4.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 runCoreMigration(i, isCleanInstall); 088 } 089 } 090 091 // ------------------------------------------------------------------------------------------------- Private Methods 092 093 private void runCoreMigration(int migrationNr, boolean isCleanInstall) { 094 runMigration(migrationNr, null, isCleanInstall); 095 dm4.pl.storeMigrationNr(migrationNr); 096 } 097 098 private void runPluginMigration(PluginImpl plugin, int migrationNr, boolean isCleanInstall) { 099 runMigration(migrationNr, plugin, isCleanInstall); 100 plugin.setMigrationNr(migrationNr); 101 } 102 103 // --- 104 105 /** 106 * Runs a core migration or a plugin migration. 107 * 108 * @param migrationNr Number of the migration to run. 109 * @param plugin The plugin that provides the migration to run. 110 * <code>null</code> for a core migration. 111 * @param isCleanInstall <code>true</code> if the migration is run as part of a clean install, 112 * <code>false</code> if the migration is run as part of an update. 113 */ 114 private void runMigration(int migrationNr, PluginImpl plugin, boolean isCleanInstall) { 115 MigrationInfo mi = null; 116 try { 117 // collect info 118 mi = new MigrationInfo(migrationNr, plugin); 119 if (!mi.success) { 120 throw mi.exception; 121 } 122 mi.checkValidity(); 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 readMigrationFile(mi.migrationIn, mi.migrationFile); 131 } else { 132 Migration migration = (Migration) mi.migrationClass.newInstance(); 133 injectServices(migration, mi.migrationInfo, plugin); 134 migration.setCoreService(dm4); 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 logger.info("Updating installed model: version " + migrationNr); 142 } catch (Exception e) { 143 throw new RuntimeException("Running " + mi.migrationInfo + " failed", e); 144 } 145 } 146 147 // --- 148 149 private void injectServices(Migration migration, String migrationInfo, PluginImpl plugin) { 150 try { 151 for (Field field : PluginImpl.getInjectableFields(migration.getClass())) { 152 Class<?> serviceInterface = field.getType(); 153 Object service; 154 // 155 if (serviceInterface.getName().equals(plugin.getProvidedServiceInterface())) { 156 // the migration consumes the plugin's own service 157 service = plugin.getContext(); 158 } else { 159 service = plugin.getInjectedService(serviceInterface); 160 } 161 // 162 logger.info("Injecting service " + serviceInterface.getName() + " into " + migrationInfo); 163 field.set(migration, service); // throws IllegalAccessException 164 } 165 } catch (Exception e) { 166 throw new RuntimeException("Injecting services into " + migrationInfo + " failed", e); 167 } 168 } 169 170 // --- 171 172 /** 173 * Creates types and topics from a JSON formatted input stream. 174 * 175 * @param migrationFileName The origin migration file. Used for logging only. 176 */ 177 private void readMigrationFile(InputStream in, String migrationFileName) { 178 try { 179 logger.info("Reading migration file \"" + migrationFileName + "\""); 180 String fileContent = JavaUtils.readText(in); 181 // 182 Object value = new JSONTokener(fileContent).nextValue(); 183 if (value instanceof JSONObject) { 184 readEntities((JSONObject) value); 185 } else if (value instanceof JSONArray) { 186 readEntities((JSONArray) value); 187 } else { 188 throw new RuntimeException("Invalid file content"); 189 } 190 } catch (Exception e) { 191 throw new RuntimeException("Reading migration file \"" + migrationFileName + "\" failed", e); 192 } 193 } 194 195 private void readEntities(JSONArray entities) throws JSONException { 196 for (int i = 0; i < entities.length(); i++) { 197 readEntities(entities.getJSONObject(i)); 198 } 199 } 200 201 private void readEntities(JSONObject entities) throws JSONException { 202 JSONArray topicTypes = entities.optJSONArray("topic_types"); 203 if (topicTypes != null) { 204 createTopicTypes(topicTypes); 205 } 206 JSONArray assocTypes = entities.optJSONArray("assoc_types"); 207 if (assocTypes != null) { 208 createAssociationTypes(assocTypes); 209 } 210 JSONArray topics = entities.optJSONArray("topics"); 211 if (topics != null) { 212 createTopics(topics); 213 } 214 JSONArray assocs = entities.optJSONArray("associations"); 215 if (assocs != null) { 216 createAssociations(assocs); 217 } 218 } 219 220 private void createTopicTypes(JSONArray topicTypes) throws JSONException { 221 for (int i = 0; i < topicTypes.length(); i++) { 222 dm4.createTopicType(mf.newTopicTypeModel(topicTypes.getJSONObject(i))); 223 } 224 } 225 226 private void createAssociationTypes(JSONArray assocTypes) throws JSONException { 227 for (int i = 0; i < assocTypes.length(); i++) { 228 dm4.createAssociationType(mf.newAssociationTypeModel(assocTypes.getJSONObject(i))); 229 } 230 } 231 232 private void createTopics(JSONArray topics) throws JSONException { 233 for (int i = 0; i < topics.length(); i++) { 234 dm4.createTopic(mf.newTopicModel(topics.getJSONObject(i))); 235 } 236 } 237 238 private void createAssociations(JSONArray assocs) throws JSONException { 239 for (int i = 0; i < assocs.length(); i++) { 240 dm4.createAssociation(mf.newAssociationModel(assocs.getJSONObject(i))); 241 } 242 } 243 244 // ------------------------------------------------------------------------------------------------- Private Classes 245 246 /** 247 * Collects the info required to run a migration. 248 */ 249 private class MigrationInfo { 250 251 String migrationType; // "core", "plugin" 252 String migrationInfo; // for logging 253 String runMode; // "CLEAN_INSTALL", "UPDATE", "ALWAYS" 254 // 255 boolean isDeclarative; 256 boolean isImperative; 257 // 258 String migrationFile; // for declarative migration 259 InputStream migrationIn; // for declarative migration 260 // 261 String migrationClassName; // for imperative migration 262 Class migrationClass; // for imperative migration 263 // 264 boolean success; // error occurred while construction? 265 Exception exception; // the error 266 267 MigrationInfo(int migrationNr, PluginImpl plugin) { 268 try { 269 String configFile = migrationConfigFile(migrationNr); 270 InputStream configIn; 271 migrationFile = migrationFile(migrationNr); 272 migrationType = plugin != null ? "plugin" : "core"; 273 // 274 if (migrationType.equals("core")) { 275 migrationInfo = "core migration " + migrationNr; 276 configIn = getClass().getResourceAsStream(configFile); 277 migrationIn = getClass().getResourceAsStream(migrationFile); 278 migrationClassName = coreMigrationClassName(migrationNr); 279 migrationClass = loadClass(migrationClassName); 280 } else { 281 migrationInfo = "migration " + migrationNr + " of " + plugin; 282 configIn = getStaticResourceOrNull(plugin, configFile); 283 migrationIn = getStaticResourceOrNull(plugin, migrationFile); 284 migrationClassName = plugin.getMigrationClassName(migrationNr); 285 if (migrationClassName != null) { 286 migrationClass = plugin.loadClass(migrationClassName); 287 } 288 } 289 // 290 isDeclarative = migrationIn != null; 291 isImperative = migrationClass != null; 292 // 293 readMigrationConfigFile(configIn, configFile); 294 // 295 success = true; 296 } catch (Exception e) { 297 exception = e; 298 } 299 } 300 301 private void checkValidity() { 302 if (!isDeclarative && !isImperative) { 303 String message = "Neither a migration file (" + migrationFile + ") nor a migration class "; 304 if (migrationClassName != null) { 305 throw new RuntimeException(message + "(" + migrationClassName + ") is found"); 306 } else { 307 throw new RuntimeException(message + "is found. Note: a possible migration class can't be located" + 308 " (plugin package is unknown). Consider setting \"dm4.plugin.main_package\" in " + 309 "plugin.properties"); 310 } 311 } 312 if (isDeclarative && isImperative) { 313 throw new RuntimeException("Ambiguity: a migration file (" + migrationFile + ") AND a migration " + 314 "class (" + migrationClassName + ") are found. Consider using two different migration numbers."); 315 } 316 } 317 318 // --- 319 320 private void readMigrationConfigFile(InputStream in, String configFile) { 321 try { 322 Properties migrationConfig = new Properties(); 323 if (in != null) { 324 logger.info("Reading migration config file \"" + configFile + "\""); 325 migrationConfig.load(in); 326 } else { 327 logger.info("Reading migration config file \"" + configFile + "\" SKIPPED -- file does not exist"); 328 } 329 // 330 runMode = migrationConfig.getProperty("migrationRunMode", MigrationRunMode.ALWAYS.name()); 331 MigrationRunMode.valueOf(runMode); // check if value is valid 332 } catch (IllegalArgumentException e) { 333 throw new RuntimeException("Reading migration config file \"" + configFile + "\" failed: \"" + runMode + 334 "\" is an invalid value for \"migrationRunMode\"", e); 335 } catch (IOException e) { 336 throw new RuntimeException("Reading migration config file \"" + configFile + "\" failed", e); 337 } 338 } 339 340 // --- 341 342 private String migrationFile(int migrationNr) { 343 return "/migrations/migration" + migrationNr + ".json"; 344 } 345 346 private String migrationConfigFile(int migrationNr) { 347 return "/migrations/migration" + migrationNr + ".properties"; 348 } 349 350 private String coreMigrationClassName(int migrationNr) { 351 return CORE_MIGRATIONS_PACKAGE + ".Migration" + migrationNr; 352 } 353 354 // --- Helper --- 355 356 private InputStream getStaticResourceOrNull(Plugin plugin, String resourceName) { 357 if (plugin.hasStaticResource(resourceName)) { 358 return plugin.getStaticResource(resourceName); 359 } else { 360 return null; 361 } 362 } 363 364 /** 365 * Uses the core bundle's class loader to load a class by name. 366 * 367 * @return the class, or <code>null</code> if the class is not found. 368 */ 369 private Class loadClass(String className) { 370 try { 371 return Class.forName(className); 372 } catch (ClassNotFoundException e) { 373 return null; 374 } 375 } 376 } 377}