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 }