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 }