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