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    }