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 REQUIRED_CORE_MIGRATION = 5;
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 = REQUIRED_CORE_MIGRATION;
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}