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