/*
* Copyright 2015 Robert von Burg <eitch@eitchnet.ch>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package li.strolch.migrations;
import java.io.File;
import java.io.FileFilter;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import li.strolch.agent.api.ComponentContainer;
import li.strolch.privilege.model.Certificate;
import li.strolch.utils.Version;
import li.strolch.utils.collections.MapOfLists;
import li.strolch.utils.dbc.DBC;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Migrations {
private static final Logger logger = LoggerFactory.getLogger(Migrations.class);
private ComponentContainer container;
private Set<String> realmNames;
private boolean verbose;
private Map<String, SortedSet<DataMigration>> dataMigrations;
private Map<String, SortedSet<CodeMigration>> codeMigrations;
private MapOfLists<String, Version> migrationsRan;
public Migrations(ComponentContainer container, Set<String> realmNames, boolean verbose) {
this.container = container;
this.realmNames = realmNames;
this.verbose = verbose;
}
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
public boolean isVerbose() {
return verbose;
}
public MapOfLists<String, Version> getMigrationsRan() {
return this.migrationsRan;
}
public void parseMigrations(File migrationsPath) {
DBC.PRE.assertTrue("If migrations path is not a directory!", migrationsPath.isDirectory());
// data migrations
this.dataMigrations = loadDataMigrations(this.realmNames, migrationsPath);
// code migrations
this.codeMigrations = loadCodeMigrations(this.realmNames, migrationsPath);
// log found migrations
if (this.verbose)
logDetectedMigrations(this.realmNames, this.dataMigrations, this.codeMigrations);
}
public void runMigrations(Certificate certificate, Map<String, MigrationVersion> currentVersions) {
MapOfLists<String, Version> migrationsRan = new MapOfLists<>();
for (Entry<String, MigrationVersion> entry : currentVersions.entrySet()) {
String realm = entry.getKey();
MigrationVersion currentVersion = entry.getValue();
if (this.verbose)
logger.info("[" + realm + "] Performing all migrations after " + currentVersion);
Version nextPossibleCodeVersion = currentVersion.getCodeVersion().add(0, 0, 1);
Version nextPossibleDataVersion = currentVersion.getDataVersion().add(0, 0, 1);
CodeMigration currentCodeMigration = new CodeMigration(realm, nextPossibleCodeVersion, null);
DataMigration currentDataMigration = new DataMigration(realm, nextPossibleDataVersion, null);
SortedSet<DataMigration> dataMigrations = this.dataMigrations.get(realm);
if (dataMigrations != null && !dataMigrations.isEmpty()) {
for (DataMigration migration : dataMigrations.tailSet(currentDataMigration)) {
String msg = "[{0}] Running data migration {1}";
logger.info(MessageFormat.format(msg, realm, migration.getVersion()));
migration.migrate(container, certificate);
migrationsRan.addElement(realm, migration.getVersion());
}
}
SortedSet<CodeMigration> codeMigrations = this.codeMigrations.get(realm);
if (codeMigrations != null && !codeMigrations.isEmpty()) {
for (CodeMigration migration : codeMigrations.tailSet(currentCodeMigration)) {
String msg = "[{0}] Running code migration {1} {2}";
logger.info(MessageFormat.format(msg, realm, migration.getVersion(), migration.getClass().getName()));
migration.migrate(container, certificate);
migrationsRan.addElement(realm, migration.getVersion());
}
}
}
if (migrationsRan.isEmpty()) {
if (this.verbose)
logger.info("There were no migrations required!");
} else {
logger.info("Migrated " + migrationsRan.size() + " realms!");
}
this.migrationsRan = migrationsRan;
}
/**
* @param cert
* @param codeMigrationsByRealm
*/
public void runCodeMigrations(Certificate cert, Map<String, MigrationVersion> currentVersions,
MapOfLists<String, CodeMigration> codeMigrationsByRealm) {
MapOfLists<String, Version> migrationsRan = new MapOfLists<>();
for (String realm : codeMigrationsByRealm.keySet()) {
// ignore if no such realm
if (!this.realmNames.contains(realm))
continue;
MigrationVersion currentVersion = currentVersions.get(realm);
List<CodeMigration> listOfMigrations = codeMigrationsByRealm.getList(realm);
SortedSet<CodeMigration> migrations = new TreeSet<>((o1, o2) -> o1.getVersion().compareTo(o2.getVersion()));
migrations.addAll(listOfMigrations);
Version nextVersion = currentVersion.getCodeVersion().add(0, 0, 1);
CodeMigration nextMigration = new CodeMigration(realm, nextVersion);
SortedSet<CodeMigration> migrationsToRun = migrations.tailSet(nextMigration);
for (CodeMigration migration : migrationsToRun) {
DBC.INTERIM.assertEquals("Realms do not match!", realm, migration.getRealm());
Version migrateVersion = migration.getVersion();
boolean isLaterMigration = migrateVersion.compareTo(currentVersion.getCodeVersion()) > 0;
DBC.INTERIM.assertTrue("Current version " + currentVersion.getCodeVersion() + " is not before next " + migrateVersion,
isLaterMigration);
String msg = "[{0}] Running code migration {1} {2}";
logger.info(MessageFormat.format(msg, realm, migrateVersion, migration.getClass().getName()));
migration.migrate(this.container, cert);
migrationsRan.addElement(realm, migration.getVersion());
}
}
if (migrationsRan.isEmpty()) {
if (this.verbose)
logger.info("There were no migrations required!");
} else {
logger.info("Migrated " + migrationsRan.size() + " realms!");
}
this.migrationsRan = migrationsRan;
}
private static void logDetectedMigrations(Set<String> realmNames,
Map<String, SortedSet<DataMigration>> allDataMigrations,
Map<String, SortedSet<CodeMigration>> allCodeMigrations) {
for (String realm : realmNames) {
SortedSet<CodeMigration> codeMigrations = allCodeMigrations.get(realm);
if (codeMigrations == null || codeMigrations.isEmpty()) {
logger.info("[" + realm + "] Found no code migrations.");
} else {
logger.info("[" + realm + "] Found " + codeMigrations.size() + " code migrations");
for (CodeMigration codeMigration : codeMigrations) {
logger.info("[" + realm + "] " + codeMigration.getVersion().toString());
}
}
SortedSet<DataMigration> dataMigrations = allDataMigrations.get(realm);
if (dataMigrations == null || dataMigrations.isEmpty()) {
logger.info("[" + realm + "] Found no data migrations.");
} else {
logger.info("[" + realm + "] Found " + dataMigrations.size() + " data migrations");
for (DataMigration dataMigration : dataMigrations) {
logger.info("[" + realm + "] " + dataMigration.getVersion().toString());
}
}
}
}
private static Map<String, SortedSet<DataMigration>> loadDataMigrations(Set<String> realmNames, File migrationsPath) {
Map<String, SortedSet<DataMigration>> migrationsByRealm = new HashMap<>();
File dataDir = new File(migrationsPath, "data");
if (dataDir.exists()) {
DBC.PRE.assertTrue("migrations/data must be a directory!", dataDir.isDirectory());
// only list directories where name is a realmName
File[] realmMigrations = dataDir.listFiles((FileFilter) path -> realmNames.contains(path.getName()));
for (File realmMigration : realmMigrations) {
String realm = realmMigration.getName();
SortedSet<DataMigration> migrations = new TreeSet<>((o1, o2) -> o1.getVersion().compareTo(
o2.getVersion()));
migrationsByRealm.put(realm, migrations);
File[] migrationFiles = realmMigration.listFiles((FileFilter) pathname -> pathname.getName().endsWith(
".xml"));
for (File file : migrationFiles) {
String name = file.getName();
Version version = Version.valueOf(name.substring(0, name.length() - 4));
migrations.add(new DataMigration(realm, version, file));
}
}
}
return migrationsByRealm;
}
private static Map<String, SortedSet<CodeMigration>> loadCodeMigrations(Set<String> realmNames, File migrationsPath) {
Map<String, SortedSet<CodeMigration>> migrationsByRealm = new HashMap<>(); //new TreeSet<>((o1, o2) -> o1.getVersion().compareTo(o2.getVersion()));
File codeDir = new File(migrationsPath, "code");
if (codeDir.exists()) {
DBC.PRE.assertTrue("migrations/code must be a directory!", codeDir.isDirectory());
File[] realmMigrations = codeDir.listFiles((FileFilter) path -> realmNames.contains(path.getName()));
for (File realmMigration : realmMigrations) {
String realm = realmMigration.getName();
SortedSet<CodeMigration> migrations = new TreeSet<>((o1, o2) -> o1.getVersion().compareTo(
o2.getVersion()));
migrationsByRealm.put(realm, migrations);
File[] migrationFiles = realmMigration.listFiles((FileFilter) pathname -> pathname.getName().endsWith(
".xml"));
for (File file : migrationFiles) {
String name = file.getName();
Version version = Version.valueOf(name.substring(0, name.length() - 4));
migrations.add(new CodeMigration(realm, version, file));
}
}
}
return migrationsByRealm;
}
public MapOfLists<String, Version> getMigrationsToRun(Map<String, MigrationVersion> currentVersions) {
MapOfLists<String, Version> migrationsToRun = new MapOfLists<>();
for (Entry<String, MigrationVersion> entry : currentVersions.entrySet()) {
String realm = entry.getKey();
Version nextPossibleCodeVersion = entry.getValue().getCodeVersion().add(0, 0, 1);
Version nextPossibleDataVersion = entry.getValue().getDataVersion().add(0, 0, 1);
CodeMigration currentCodeMigration = new CodeMigration(realm, nextPossibleCodeVersion, null);
DataMigration currentDataMigration = new DataMigration(realm, nextPossibleDataVersion, null);
SortedSet<CodeMigration> allCodeMigrations = this.codeMigrations.get(realm);
if (allCodeMigrations != null) {
SortedSet<CodeMigration> codeMigrations = allCodeMigrations.tailSet(currentCodeMigration);
for (CodeMigration codeMigration : codeMigrations) {
if (!migrationsToRun.containsElement(realm, codeMigration.getVersion()))
migrationsToRun.addElement(realm, codeMigration.getVersion());
}
}
SortedSet<DataMigration> allDataMigrations = this.dataMigrations.get(realm);
if (allDataMigrations != null) {
SortedSet<DataMigration> dataMigrations = allDataMigrations.tailSet(currentDataMigration);
for (DataMigration dataMigration : dataMigrations) {
if (!migrationsToRun.containsElement(realm, dataMigration.getVersion()))
migrationsToRun.addElement(realm, dataMigration.getVersion());
}
}
}
return migrationsToRun;
}
}