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}