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