package com.constellio.app.services.migrations;
import static com.constellio.model.entities.records.wrappers.Collection.SYSTEM_COLLECTION;
import static java.util.Arrays.asList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.constellio.app.entities.modules.ComboMigrationScript;
import com.constellio.app.entities.modules.InstallableModule;
import com.constellio.app.entities.modules.Migration;
import com.constellio.app.entities.modules.MigrationResourcesProvider;
import com.constellio.app.entities.modules.MigrationScript;
import com.constellio.app.entities.modules.ModuleWithComboMigration;
import com.constellio.app.entities.modules.locators.ModuleResourcesLocator;
import com.constellio.app.entities.modules.locators.PropertiesLocatorFactory;
import com.constellio.app.services.collections.CollectionsManager;
import com.constellio.app.services.extensions.ConstellioModulesManagerImpl;
import com.constellio.app.services.extensions.plugins.ConstellioPluginManager;
import com.constellio.app.services.factories.AppLayerFactory;
import com.constellio.data.dao.managers.config.ConfigManager;
import com.constellio.data.dao.managers.config.ConfigManagerException.OptimisticLockingConfiguration;
import com.constellio.data.dao.managers.config.PropertiesAlteration;
import com.constellio.data.dao.managers.config.values.PropertiesConfiguration;
import com.constellio.data.dao.services.factories.DataLayerFactory;
import com.constellio.data.io.services.facades.IOServices;
import com.constellio.model.entities.Language;
import com.constellio.model.entities.records.wrappers.Collection;
import com.constellio.model.services.factories.ModelLayerFactory;
import com.constellio.model.services.schemas.MetadataSchemasManager;
import com.constellio.model.services.schemas.MetadataSchemasManagerException.OptimisticLocking;
import com.constellio.model.services.schemas.builders.CommonMetadataBuilder;
import com.constellio.model.services.schemas.builders.MetadataSchemaTypesBuilder;
public class MigrationServices {
private static final Logger LOGGER = LoggerFactory.getLogger(MigrationServices.class);
private static final String VERSION_PROPERTIES_FILE = "/version.properties";
ConfigManager configManager;
ConstellioEIM constellioEIM;
AppLayerFactory appLayerFactory;
ConstellioModulesManagerImpl constellioModulesManager;
ConstellioPluginManager constellioPluginManager;
DataLayerFactory dataLayerFactory;
ModelLayerFactory modelLayerFactory;
ModuleResourcesLocator moduleResourcesLocator;
CollectionsManager collectionsManager;
public MigrationServices(ConstellioEIM constellioEIM, AppLayerFactory appLayerFactory,
ConstellioModulesManagerImpl constellioModulesManager, ConstellioPluginManager constellioPluginManager) {
super();
this.constellioEIM = constellioEIM;
this.appLayerFactory = appLayerFactory;
this.modelLayerFactory = appLayerFactory.getModelLayerFactory();
this.dataLayerFactory = modelLayerFactory.getDataLayerFactory();
this.constellioModulesManager = constellioModulesManager;
this.constellioPluginManager = constellioPluginManager;
this.configManager = dataLayerFactory.getConfigManager();
this.collectionsManager = appLayerFactory.getCollectionsManager();
this.moduleResourcesLocator = PropertiesLocatorFactory.get();
}
private void addPropertiesFileWithVersion(String collection, String version, Map<String, String> properties) {
properties.put(collection + "_version", version);
configManager.add(VERSION_PROPERTIES_FILE, properties);
}
public String getCurrentVersion(String collection) {
PropertiesConfiguration propertiesConfiguration = configManager.getProperties(VERSION_PROPERTIES_FILE);
return propertiesConfiguration == null ? null : propertiesConfiguration.getProperties().get(collection + "_version");
}
private List<Migration> getAllMigrationsFor(boolean newCollection, String collection) {
ConstellioModulesManagerImpl modulesManager = getModulesManager();
List<Migration> migrations = new ArrayList<>();
List<InstallableModule> enabledModules, requiredDependentModules;
enabledModules = modulesManager.getEnabledModules(collection);
requiredDependentModules = modulesManager.getRequiredDependentModulesToInstall(collection);
List<InstallableModule> modules = new ArrayList<>(enabledModules);
for (InstallableModule installableModule : requiredDependentModules) {
if (!modulesManager.isInstalled(installableModule)) {
modulesManager.markAsInstalled(installableModule,
appLayerFactory.getModelLayerFactory().getCollectionsListManager());
}
if (!modulesManager.isModuleEnabled(collection, installableModule)) {
modulesManager.markAsEnabled(installableModule, collection);
}
modules.add(installableModule);
}
for (InstallableModule module : modules) {
boolean useComboMigration = newCollection && module instanceof ModuleWithComboMigration;
if (useComboMigration) {
ComboMigrationScript comboMigrationScript = ((ModuleWithComboMigration) module).getComboMigrationScript();
List<String> completedMigrations = getCompletedMigrations(collection);
for (MigrationScript aMigrationScriptIncludedInCombo : comboMigrationScript.getVersions()) {
if (completedMigrations.contains(
new Migration(collection,module.getId(),aMigrationScriptIncludedInCombo).getMigrationId())) {
useComboMigration = false;
break;
}
}
}
if (useComboMigration) {
ComboMigrationScript comboMigrationScript = ((ModuleWithComboMigration) module).getComboMigrationScript();
migrations.add(new Migration(collection, module.getId(), comboMigrationScript));
for (MigrationScript migrationScript : getMigrationScripts(module)) {
boolean found = false;
for (MigrationScript migrationScriptInCombo : comboMigrationScript.getVersions()) {
if (migrationScriptInCombo.getClass().equals(migrationScript.getClass())) {
found = true;
break;
}
}
if (!found) {
migrations.add(new Migration(collection, module.getId(), migrationScript));
}
}
} else {
for (MigrationScript script : getMigrationScripts(module)) {
migrations.add(new Migration(collection, module.getId(), script));
}
}
}
for (
MigrationScript script
: constellioEIM.getMigrationScripts())
{
migrations.add(new Migration(collection, null, script));
}
Collections.sort(migrations, MigrationScriptsComparator.forModules(modules));
return migrations;
}
private List<MigrationScript> getMigrationScripts(InstallableModule module) {
List<MigrationScript> returnList = new ArrayList<>();
try {
returnList = module.getMigrationScripts();
} catch (Throwable e) {
LOGGER.warn("Error when trying get module " + module.getId() + " migration scripts");
}
return returnList;
}
public Set<String> migrate(String toVersion, boolean newModule)
throws OptimisticLockingConfiguration {
Set<String> modulesNotMigratedCorrectly = new HashSet<>();
List<String> collections = modelLayerFactory.getCollectionsListManager().getCollections();
for (String collection : collections) {
modulesNotMigratedCorrectly.addAll(migrate(collection, toVersion, newModule));
}
return modulesNotMigratedCorrectly;
}
private Set<String> migrateModules(String collection, String toVersion, boolean newModule)
throws OptimisticLockingConfiguration {
Set<String> modulesNotMigratedCorrectly = new HashSet<>();
List<String> collectionCodes = collectionsManager.getCollectionCodesExcludingSystem();
boolean newCollection = isNewCollection(collection);
if (newCollection && appLayerFactory.getAppLayerConfiguration().isFastMigrationsEnabled() &&
(!SYSTEM_COLLECTION.equals(collection) || collectionCodes.isEmpty())) {
migrateWithoutException(new CoreMigrationCombo(), null, collection);
}
List<Migration> migrations;
boolean firstMigration = true;
boolean fastMigrationEnabled = appLayerFactory.getAppLayerConfiguration().isFastMigrationsEnabled();
while (modulesNotMigratedCorrectly.isEmpty() &&
!(migrations = filterRunnedMigration(collection,
getAllMigrationsFor(newModule && fastMigrationEnabled, collection))).isEmpty()) {
LOGGER.info("Migrating collection " + collection + " : " + migrations);
for (Migration migration : migrations) {
if (toVersion == null || VersionsComparator
.isFirstVersionBeforeOrEqualToSecond(migration.getVersion(), toVersion)) {
if (firstMigration) {
ensureSchemasHaveCommonMetadata(collection);
firstMigration = false;
}
boolean exceptionWhenMigrating = migrateWithoutException(migration, collection);
if (exceptionWhenMigrating) {
modulesNotMigratedCorrectly.add(migration.getMigrationId());
}
}
}
}
return modulesNotMigratedCorrectly;
}
private boolean migrateWithoutException(ComboMigrationScript migration, String moduleId, String collection) {
boolean exceptionWhenMigrating = false;
try {
migrate(collection, moduleId, migration);
} catch (Throwable e) {
constellioPluginManager
.handleModuleNotMigratedCorrectly(moduleId, collection, e);
exceptionWhenMigrating = true;
}
return exceptionWhenMigrating;
}
private boolean migrateWithoutException(Migration migration, String collection) {
boolean exceptionWhenMigrating = false;
try {
if (migration.getScript() instanceof ComboMigrationScript) {
migrate(collection, migration.getModuleId(), (ComboMigrationScript) migration.getScript());
} else {
migrate(migration);
}
} catch (Throwable e) {
if (dataLayerFactory.getTransactionLogRecoveryManager().isInRollbackMode()) {
throw new RuntimeException("A migration error is triggering a rollback", e);
} else {
constellioPluginManager
.handleModuleNotMigratedCorrectly(migration.getModuleId(), collection, e);
exceptionWhenMigrating = true;
}
}
return exceptionWhenMigrating;
}
public Set<String> migrate(String collection, String toVersion, boolean newModule)
throws OptimisticLockingConfiguration {
return migrateModules(collection, toVersion, newModule);
}
boolean isNewCollection(String collection) {
if (configManager.exist(VERSION_PROPERTIES_FILE)) {
String propertyKey = collection + "_completedMigrations";
String completedMigrations = configManager.getProperties(VERSION_PROPERTIES_FILE).getProperties().get(propertyKey);
return StringUtils.isBlank(completedMigrations);
} else {
return true;
}
}
List<Migration> filterRunnedMigration(String collection, List<Migration> migrations) {
if (configManager.exist(VERSION_PROPERTIES_FILE)) {
String propertyKey = collection + "_completedMigrations";
String completedMigrations = dataLayerFactory.getConfigManager().getProperties(VERSION_PROPERTIES_FILE)
.getProperties().get(propertyKey);
if (completedMigrations == null) {
return migrations;
} else {
List<Migration> filteredMigrations = new ArrayList<>();
for (Migration migration : migrations) {
if (!completedMigrations.contains(migration.getMigrationId())) {
filteredMigrations.add(migration);
}
}
return filteredMigrations;
}
} else {
return migrations;
}
}
private void ensureSchemasHaveCommonMetadata(String collection) {
MetadataSchemasManager manager = modelLayerFactory.getMetadataSchemasManager();
MetadataSchemaTypesBuilder types = manager.modify(collection);
new CommonMetadataBuilder().addCommonMetadataToAllExistingSchemas(types);
try {
manager.saveUpdateSchemaTypes(types);
} catch (OptimisticLocking e) {
ensureSchemasHaveCommonMetadata(collection);
}
}
void migrate(Migration migration)
throws OptimisticLockingConfiguration {
MigrationScript script = migration.getScript();
LOGGER.info("Running migration script '" + script.getClass().getSimpleName() +
"' updating to version '" + script.getVersion() + "'");
IOServices ioServices = modelLayerFactory.getDataLayerFactory().getIOServicesFactory().newIOServices();
Language language = Language.withCode(modelLayerFactory.getConfiguration().getMainDataLanguage());
String moduleId = migration.getModuleId() == null ? "core" : migration.getModuleId();
String version = migration.getVersion();
List<Language> languages = Language.withCodes(collectionsManager.getCollectionLanguages(migration.getCollection()));
MigrationResourcesProvider migrationResourcesProvider = new MigrationResourcesProvider(moduleId, language, languages,
version, ioServices, moduleResourcesLocator);
try {
script.migrate(migration.getCollection(), migrationResourcesProvider, appLayerFactory);
} catch (Exception e) {
throw new RuntimeException("Error when migrating collection '" + migration.getCollection() + "'", e);
}
setCurrentDataVersion(migration.getCollection(), migration.getVersion());
markMigrationAsCompleted(migration);
}
String getHighestVersion(ComboMigrationScript script) {
List<String> versions = new ArrayList<>();
if (script.getVersions().isEmpty()) {
return "1.0";
}
for (MigrationScript migration : script.getVersions()) {
versions.add(migration.getVersion());
}
Collections.sort(versions, new VersionsComparator());
return versions.get(versions.size() - 1);
}
void migrate(String collectionId, String moduleId, ComboMigrationScript fastMigrationScript)
throws OptimisticLockingConfiguration {
String highestVersion = getHighestVersion(fastMigrationScript);
LOGGER.info("Running migration script '" + fastMigrationScript.getClass().getSimpleName() +
"' updating to version '" + highestVersion + "'");
IOServices ioServices = modelLayerFactory.getDataLayerFactory().getIOServicesFactory().newIOServices();
Language language = Language.withCode(modelLayerFactory.getConfiguration().getMainDataLanguage());
List<Language> languages = Language.withCodes(collectionsManager.getCollectionLanguages(collectionId));
moduleId = moduleId == null ? "core" : moduleId;
MigrationResourcesProvider migrationResourcesProvider = new MigrationResourcesProvider(moduleId, language, languages,
"combo", ioServices, moduleResourcesLocator);
try {
fastMigrationScript.migrate(collectionId, migrationResourcesProvider, appLayerFactory);
} catch (Exception e) {
throw new RuntimeException("Error when migrating collection '" + collectionId + "'", e);
}
setCurrentDataVersion(collectionId, highestVersion);
for (MigrationScript migrationScript : fastMigrationScript.getVersions()) {
markMigrationAsCompleted(new Migration(collectionId, moduleId, migrationScript));
}
}
public void setCurrentDataVersion(String collection, String version)
throws OptimisticLockingConfiguration {
Map<String, String> properties = new HashMap<String, String>();
if (dataLayerFactory.getConfigManager().exist(VERSION_PROPERTIES_FILE)) {
updateVersionOfExistancePropertiesFile(collection, version);
} else {
addPropertiesFileWithVersion(collection, version, properties);
}
}
public List<String> getCompletedMigrations(String collection) {
Map<String, String> properties = dataLayerFactory.getConfigManager()
.getProperties(VERSION_PROPERTIES_FILE).getProperties();
String completedMigrations = properties.get(collection + "_completedMigrations");
if (StringUtils.isNotBlank(completedMigrations)) {
return asList(completedMigrations.split(","));
} else {
return Collections.emptyList();
}
}
public void markMigrationAsCompleted(final Migration migration)
throws OptimisticLockingConfiguration {
final String propertyKey = migration.getCollection() + "_completedMigrations";
dataLayerFactory.getConfigManager().updateProperties(VERSION_PROPERTIES_FILE, new PropertiesAlteration() {
@Override
public void alter(Map<String, String> properties) {
String completedMigrations = properties.get(propertyKey);
List<String> migrations = new ArrayList<String>();
if (StringUtils.isNotBlank(completedMigrations)) {
migrations.addAll(asList(completedMigrations.split(",")));
}
migrations.add(migration.getMigrationId());
Collections.sort(migrations);
properties.put(propertyKey, StringUtils.join(migrations, ","));
}
});
}
private void updateVersionOfExistancePropertiesFile(String collection, String version)
throws OptimisticLockingConfiguration {
Map<String, String> properties;
properties = dataLayerFactory.getConfigManager().getProperties(VERSION_PROPERTIES_FILE).getProperties();
properties.put(collection + "_version", version);
dataLayerFactory.getConfigManager().update(VERSION_PROPERTIES_FILE,
dataLayerFactory.getConfigManager().getProperties(VERSION_PROPERTIES_FILE).getHash(), properties);
}
private ConstellioPluginManager getPluginManager() {
return constellioPluginManager;
}
private ConstellioModulesManagerImpl getModulesManager() {
return constellioModulesManager;
}
public static class InvalidPluginModule extends Exception {
final private List<InstallableModule> invalidModules;
public InvalidPluginModule(InstallableModule... invalidModules) {
this.invalidModules = new ArrayList<>();
for (InstallableModule invalidModule : invalidModules) {
this.invalidModules.add(invalidModule);
}
}
public List<InstallableModule> getInvalidModules() {
return invalidModules;
}
}
}