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