001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.service.Migration;
004    import de.deepamehta.core.service.Plugin;
005    import de.deepamehta.core.util.DeepaMehtaUtils;
006    
007    import java.io.InputStream;
008    import java.io.IOException;
009    import java.util.Properties;
010    import java.util.logging.Logger;
011    
012    
013    
014    class MigrationManager {
015    
016        // ------------------------------------------------------------------------------------------------------- Constants
017    
018        private static final String CORE_MIGRATIONS_PACKAGE = "de.deepamehta.core.migrations";
019        private static final int REQUIRED_CORE_MIGRATION = 3;
020    
021        // ---------------------------------------------------------------------------------------------- Instance Variables
022    
023        private EmbeddedService dms;
024    
025        private enum MigrationRunMode {
026            CLEAN_INSTALL, UPDATE, ALWAYS
027        }
028    
029        private Logger logger = Logger.getLogger(getClass().getName());
030    
031        // ---------------------------------------------------------------------------------------------------- Constructors
032    
033        MigrationManager(EmbeddedService dms) {
034            this.dms = dms;
035        }
036    
037        // ----------------------------------------------------------------------------------------- Package Private Methods
038    
039        /**
040         * Determines the migrations to be run for the specified plugin and run them.
041         */
042        void runPluginMigrations(PluginImpl plugin, boolean isCleanInstall) {
043            int migrationNr = plugin.getPluginTopic().getChildTopics().getTopic("dm4.core.plugin_migration_nr")
044                .getSimpleValue().intValue();
045            int requiredMigrationNr = Integer.parseInt(plugin.getConfigProperty("requiredPluginMigrationNr", "0"));
046            int migrationsToRun = requiredMigrationNr - migrationNr;
047            //
048            if (migrationsToRun == 0) {
049                logger.info("Running migrations for " + plugin + " ABORTED -- everything up-to-date (migrationNr=" +
050                    migrationNr + ")");
051                return;
052            }
053            //
054            logger.info("Running " + migrationsToRun + " migrations for " + plugin + " (migrationNr=" + migrationNr +
055                ", requiredMigrationNr=" + requiredMigrationNr + ")");
056            for (int i = migrationNr + 1; i <= requiredMigrationNr; i++) {
057                runPluginMigration(plugin, i, isCleanInstall);
058            }
059        }
060    
061        /**
062         * Determines the core migrations to be run and run them.
063         */
064        void runCoreMigrations(boolean isCleanInstall) {
065            int migrationNr = dms.storageDecorator.fetchMigrationNr();
066            int requiredMigrationNr = REQUIRED_CORE_MIGRATION;
067            int migrationsToRun = requiredMigrationNr - migrationNr;
068            //
069            if (migrationsToRun == 0) {
070                logger.info("Running core migrations ABORTED -- everything up-to-date (migrationNr=" + migrationNr + ")");
071                return;
072            }
073            //
074            logger.info("Running " + migrationsToRun + " core migrations (migrationNr=" + migrationNr +
075                ", requiredMigrationNr=" + requiredMigrationNr + ")");
076            for (int i = migrationNr + 1; i <= requiredMigrationNr; i++) {
077                runCoreMigration(i, isCleanInstall);
078            }
079        }
080    
081        // ------------------------------------------------------------------------------------------------- Private Methods
082    
083        private void runCoreMigration(int migrationNr, boolean isCleanInstall) {
084            runMigration(migrationNr, null, isCleanInstall);
085            dms.storageDecorator.storeMigrationNr(migrationNr);
086        }
087    
088        private void runPluginMigration(PluginImpl plugin, int migrationNr, boolean isCleanInstall) {
089            runMigration(migrationNr, plugin, isCleanInstall);
090            plugin.setMigrationNr(migrationNr);
091        }
092    
093        // ---
094    
095        /**
096         * Runs a core migration or a plugin migration.
097         *
098         * @param   migrationNr     Number of the migration to run.
099         * @param   plugin          The plugin that provides the migration to run.
100         *                          <code>null</code> for a core migration.
101         * @param   isCleanInstall  <code>true</code> if the migration is run as part of a clean install,
102         *                          <code>false</code> if the migration is run as part of an update.
103         */
104        private void runMigration(int migrationNr, PluginImpl plugin, boolean isCleanInstall) {
105            MigrationInfo mi = null;
106            try {
107                mi = new MigrationInfo(migrationNr, plugin);
108                if (!mi.success) {
109                    throw mi.exception;
110                }
111                // error checks
112                if (!mi.isDeclarative && !mi.isImperative) {
113                    String message = "Neither a migration file (" + mi.migrationFile + ") nor a migration class ";
114                    if (mi.migrationClassName != null) {
115                        throw new RuntimeException(message + "(" + mi.migrationClassName + ") is found");
116                    } else {
117                        throw new RuntimeException(message + "is found. Note: a possible migration class can't be located" +
118                            " (plugin package is unknown). Consider setting \"pluginPackage\" in plugin.properties");
119                    }
120                }
121                if (mi.isDeclarative && mi.isImperative) {
122                    throw new RuntimeException("Ambiguity: a migration file (" + mi.migrationFile + ") AND a migration " +
123                        "class (" + mi.migrationClassName + ") are found. Consider using two different migration numbers.");
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                        DeepaMehtaUtils.readMigrationFile(mi.migrationIn, mi.migrationFile, dms);
132                    } else {
133                        Migration migration = (Migration) mi.migrationClass.newInstance();
134                        logger.info("Running " + mi.migrationType + " migration class " + mi.migrationClassName);
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        // ------------------------------------------------------------------------------------------------- Private Classes
149    
150        /**
151         * Collects the info required to run a migration.
152         */
153        private class MigrationInfo {
154    
155            String migrationType;       // "core", "plugin"
156            String migrationInfo;       // for logging
157            String runMode;             // "CLEAN_INSTALL", "UPDATE", "ALWAYS"
158            //
159            boolean isDeclarative;
160            boolean isImperative;
161            //
162            String migrationFile;       // for declarative migration
163            InputStream migrationIn;    // for declarative migration
164            //
165            String migrationClassName;  // for imperative migration
166            Class migrationClass;       // for imperative migration
167            //
168            boolean success;            // error occurred?
169            Exception exception;        // the error
170    
171            MigrationInfo(int migrationNr, PluginImpl plugin) {
172                try {
173                    String configFile = migrationConfigFile(migrationNr);
174                    InputStream configIn;
175                    migrationFile = migrationFile(migrationNr);
176                    migrationType = plugin != null ? "plugin" : "core";
177                    //
178                    if (migrationType.equals("core")) {
179                        migrationInfo = "core migration " + migrationNr;
180                        configIn     = getClass().getResourceAsStream(configFile);
181                        migrationIn  = getClass().getResourceAsStream(migrationFile);
182                        migrationClassName = coreMigrationClassName(migrationNr);
183                        migrationClass = loadClass(migrationClassName);
184                    } else {
185                        migrationInfo = "migration " + migrationNr + " of " + plugin;
186                        configIn     = getStaticResourceOrNull(plugin, configFile);
187                        migrationIn  = getStaticResourceOrNull(plugin, migrationFile);
188                        migrationClassName = plugin.getMigrationClassName(migrationNr);
189                        if (migrationClassName != null) {
190                            migrationClass = plugin.loadClass(migrationClassName);
191                        }
192                    }
193                    //
194                    isDeclarative = migrationIn != null;
195                    isImperative = migrationClass != null;
196                    //
197                    readMigrationConfigFile(configIn, configFile);
198                    //
199                    success = true;
200                } catch (Exception e) {
201                    exception = e;
202                }
203            }
204    
205            // ---
206    
207            private void readMigrationConfigFile(InputStream in, String configFile) {
208                try {
209                    Properties migrationConfig = new Properties();
210                    if (in != null) {
211                        logger.info("Reading migration config file \"" + configFile + "\"");
212                        migrationConfig.load(in);
213                    } else {
214                        logger.info("Reading migration config file \"" + configFile + "\" ABORTED -- file does not exist");
215                    }
216                    //
217                    runMode = migrationConfig.getProperty("migrationRunMode", MigrationRunMode.ALWAYS.name());
218                    MigrationRunMode.valueOf(runMode);  // check if value is valid
219                } catch (IllegalArgumentException e) {
220                    throw new RuntimeException("Reading migration config file \"" + configFile + "\" failed: \"" + runMode +
221                        "\" is an invalid value for \"migrationRunMode\"", e);
222                } catch (IOException e) {
223                    throw new RuntimeException("Reading migration config file \"" + configFile + "\" failed", e);
224                }
225            }
226    
227            // ---
228    
229            private String migrationFile(int migrationNr) {
230                return "/migrations/migration" + migrationNr + ".json";
231            }
232    
233            private String migrationConfigFile(int migrationNr) {
234                return "/migrations/migration" + migrationNr + ".properties";
235            }
236    
237            private String coreMigrationClassName(int migrationNr) {
238                return CORE_MIGRATIONS_PACKAGE + ".Migration" + migrationNr;
239            }
240    
241            // --- Helper ---
242    
243            private InputStream getStaticResourceOrNull(Plugin plugin, String resourceName) {
244                if (plugin.hasStaticResource(resourceName)) {
245                    return plugin.getStaticResource(resourceName);
246                } else {
247                    return null;
248                }
249            }
250    
251            /**
252             * Uses the core bundle's class loader to load a class by name.
253             *
254             * @return  the class, or <code>null</code> if the class is not found.
255             */
256            private Class loadClass(String className) {
257                try {
258                    return Class.forName(className);
259                } catch (ClassNotFoundException e) {
260                    return null;
261                }
262            }
263        }
264    }