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 }