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 }