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.Plugin; 009import de.deepamehta.core.util.JavaUtils; 010 011import org.codehaus.jettison.json.JSONArray; 012import org.codehaus.jettison.json.JSONException; 013import org.codehaus.jettison.json.JSONObject; 014import org.codehaus.jettison.json.JSONTokener; 015 016import java.io.InputStream; 017import java.io.IOException; 018import java.lang.reflect.Field; 019import java.util.Properties; 020import java.util.logging.Logger; 021 022 023 024class MigrationManager { 025 026 // ------------------------------------------------------------------------------------------------------- Constants 027 028 private static final String CORE_MIGRATIONS_PACKAGE = "de.deepamehta.core.migrations"; 029 private static final int REQUIRED_CORE_MIGRATION = 5; 030 031 // ---------------------------------------------------------------------------------------------- Instance Variables 032 033 private EmbeddedService dms; 034 035 private enum MigrationRunMode { 036 CLEAN_INSTALL, UPDATE, ALWAYS 037 } 038 039 private Logger logger = Logger.getLogger(getClass().getName()); 040 041 // ---------------------------------------------------------------------------------------------------- Constructors 042 043 MigrationManager(EmbeddedService dms) { 044 this.dms = dms; 045 } 046 047 // ----------------------------------------------------------------------------------------- Package Private Methods 048 049 /** 050 * Determines the migrations to be run for the specified plugin and run them. 051 */ 052 void runPluginMigrations(PluginImpl plugin, boolean isCleanInstall) { 053 int migrationNr = plugin.getPluginTopic().getChildTopics().getTopic("dm4.core.plugin_migration_nr") 054 .getSimpleValue().intValue(); 055 int requiredMigrationNr = Integer.parseInt(plugin.getConfigProperty("requiredPluginMigrationNr", "0")); 056 int migrationsToRun = requiredMigrationNr - migrationNr; 057 // 058 if (migrationsToRun == 0) { 059 logger.info("Running migrations for " + plugin + " ABORTED -- everything up-to-date (migrationNr=" + 060 migrationNr + ")"); 061 return; 062 } 063 // 064 logger.info("Running " + migrationsToRun + " migrations for " + plugin + " (migrationNr=" + migrationNr + 065 ", requiredMigrationNr=" + requiredMigrationNr + ")"); 066 for (int i = migrationNr + 1; i <= requiredMigrationNr; i++) { 067 runPluginMigration(plugin, i, isCleanInstall); 068 } 069 } 070 071 /** 072 * Determines the core migrations to be run and run them. 073 */ 074 void runCoreMigrations(boolean isCleanInstall) { 075 int migrationNr = dms.storageDecorator.fetchMigrationNr(); 076 int requiredMigrationNr = REQUIRED_CORE_MIGRATION; 077 int migrationsToRun = requiredMigrationNr - migrationNr; 078 // 079 if (migrationsToRun == 0) { 080 logger.info("Running core migrations ABORTED -- everything up-to-date (migrationNr=" + migrationNr + ")"); 081 return; 082 } 083 // 084 logger.info("Running " + migrationsToRun + " core migrations (migrationNr=" + migrationNr + 085 ", requiredMigrationNr=" + requiredMigrationNr + ")"); 086 for (int i = migrationNr + 1; i <= requiredMigrationNr; 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 dms.storageDecorator.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 logger.info("Running " + mi.migrationType + " migration class " + mi.migrationClassName); 134 injectServices(migration, mi.migrationInfo, plugin); 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 // --- 149 150 private void injectServices(Migration migration, String migrationInfo, PluginImpl plugin) { 151 try { 152 for (Field field : PluginImpl.getInjectableFields(migration.getClass())) { 153 Class<?> serviceInterface = field.getType(); 154 Object service; 155 // 156 if (serviceInterface.getName().equals(plugin.getProvidedServiceInterface())) { 157 // the migration consumes the plugin's own service 158 service = plugin.getContext(); 159 } else { 160 service = plugin.getInjectedService(serviceInterface); 161 } 162 // 163 logger.info("Injecting service " + serviceInterface.getName() + " into " + migrationInfo); 164 field.set(migration, service); // throws IllegalAccessException 165 } 166 } catch (Exception e) { 167 throw new RuntimeException("Injecting services into " + migrationInfo + " failed", e); 168 } 169 } 170 171 // --- 172 173 /** 174 * Creates types and topics from a JSON formatted input stream. 175 * 176 * @param migrationFileName The origin migration file. Used for logging only. 177 */ 178 private void readMigrationFile(InputStream in, String migrationFileName) { 179 try { 180 logger.info("Reading migration file \"" + migrationFileName + "\""); 181 String fileContent = JavaUtils.readText(in); 182 // 183 Object value = new JSONTokener(fileContent).nextValue(); 184 if (value instanceof JSONObject) { 185 readEntities((JSONObject) value); 186 } else if (value instanceof JSONArray) { 187 readEntities((JSONArray) value); 188 } else { 189 throw new RuntimeException("Invalid file content"); 190 } 191 } catch (Exception e) { 192 throw new RuntimeException("Reading migration file \"" + migrationFileName + "\" failed", e); 193 } 194 } 195 196 private void readEntities(JSONArray entities) throws JSONException { 197 for (int i = 0; i < entities.length(); i++) { 198 readEntities(entities.getJSONObject(i)); 199 } 200 } 201 202 private void readEntities(JSONObject entities) throws JSONException { 203 JSONArray topicTypes = entities.optJSONArray("topic_types"); 204 if (topicTypes != null) { 205 createTopicTypes(topicTypes); 206 } 207 JSONArray assocTypes = entities.optJSONArray("assoc_types"); 208 if (assocTypes != null) { 209 createAssociationTypes(assocTypes); 210 } 211 JSONArray topics = entities.optJSONArray("topics"); 212 if (topics != null) { 213 createTopics(topics); 214 } 215 JSONArray assocs = entities.optJSONArray("associations"); 216 if (assocs != null) { 217 createAssociations(assocs); 218 } 219 } 220 221 private void createTopicTypes(JSONArray topicTypes) throws JSONException { 222 for (int i = 0; i < topicTypes.length(); i++) { 223 dms.createTopicType(new TopicTypeModel(topicTypes.getJSONObject(i))); 224 } 225 } 226 227 private void createAssociationTypes(JSONArray assocTypes) throws JSONException { 228 for (int i = 0; i < assocTypes.length(); i++) { 229 dms.createAssociationType(new AssociationTypeModel(assocTypes.getJSONObject(i))); 230 } 231 } 232 233 private void createTopics(JSONArray topics) throws JSONException { 234 for (int i = 0; i < topics.length(); i++) { 235 dms.createTopic(new TopicModel(topics.getJSONObject(i))); 236 } 237 } 238 239 private void createAssociations(JSONArray assocs) throws JSONException { 240 for (int i = 0; i < assocs.length(); i++) { 241 dms.createAssociation(new AssociationModel(assocs.getJSONObject(i))); 242 } 243 } 244 245 // ------------------------------------------------------------------------------------------------- Private Classes 246 247 /** 248 * Collects the info required to run a migration. 249 */ 250 private class MigrationInfo { 251 252 String migrationType; // "core", "plugin" 253 String migrationInfo; // for logging 254 String runMode; // "CLEAN_INSTALL", "UPDATE", "ALWAYS" 255 // 256 boolean isDeclarative; 257 boolean isImperative; 258 // 259 String migrationFile; // for declarative migration 260 InputStream migrationIn; // for declarative migration 261 // 262 String migrationClassName; // for imperative migration 263 Class migrationClass; // for imperative migration 264 // 265 boolean success; // error occurred while construction? 266 Exception exception; // the error 267 268 MigrationInfo(int migrationNr, PluginImpl plugin) { 269 try { 270 String configFile = migrationConfigFile(migrationNr); 271 InputStream configIn; 272 migrationFile = migrationFile(migrationNr); 273 migrationType = plugin != null ? "plugin" : "core"; 274 // 275 if (migrationType.equals("core")) { 276 migrationInfo = "core migration " + migrationNr; 277 configIn = getClass().getResourceAsStream(configFile); 278 migrationIn = getClass().getResourceAsStream(migrationFile); 279 migrationClassName = coreMigrationClassName(migrationNr); 280 migrationClass = loadClass(migrationClassName); 281 } else { 282 migrationInfo = "migration " + migrationNr + " of " + plugin; 283 configIn = getStaticResourceOrNull(plugin, configFile); 284 migrationIn = getStaticResourceOrNull(plugin, migrationFile); 285 migrationClassName = plugin.getMigrationClassName(migrationNr); 286 if (migrationClassName != null) { 287 migrationClass = plugin.loadClass(migrationClassName); 288 } 289 } 290 // 291 isDeclarative = migrationIn != null; 292 isImperative = migrationClass != null; 293 // 294 readMigrationConfigFile(configIn, configFile); 295 // 296 success = true; 297 } catch (Exception e) { 298 exception = e; 299 } 300 } 301 302 private void checkValidity() { 303 if (!isDeclarative && !isImperative) { 304 String message = "Neither a migration file (" + migrationFile + ") nor a migration class "; 305 if (migrationClassName != null) { 306 throw new RuntimeException(message + "(" + migrationClassName + ") is found"); 307 } else { 308 throw new RuntimeException(message + "is found. Note: a possible migration class can't be located" + 309 " (plugin package is unknown). Consider setting \"pluginPackage\" in 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 + "\" ABORTED -- 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}