org.eclipse.che.core.db.schema.impl.flyway.CustomSqlMigrationResolver.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.che.core.db.schema.impl.flyway.CustomSqlMigrationResolver.java

Source

/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.core.db.schema.impl.flyway;

import com.google.common.hash.Hashing;
import com.google.common.io.ByteSource;

import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.MigrationType;
import org.flywaydb.core.api.MigrationVersion;
import org.flywaydb.core.api.resolver.BaseMigrationResolver;
import org.flywaydb.core.api.resolver.ResolvedMigration;
import org.flywaydb.core.internal.dbsupport.DbSupport;
import org.flywaydb.core.internal.resolver.ResolvedMigrationImpl;
import org.flywaydb.core.internal.resolver.sql.SqlMigrationExecutor;
import org.flywaydb.core.internal.util.Location;
import org.flywaydb.core.internal.util.PlaceholderReplacer;
import org.flywaydb.core.internal.util.scanner.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static java.lang.String.format;
import static java.util.stream.Collectors.toList;

/**
 * Resolves SQL migrations from the configured locations,
 * allows overriding of default scripts with vendor specific ones.
 *
 * <ul>Migration scripts must follow the next rules:
 * <li>It must be placed in the project dir directory e.g. <i>5.0.1</i></li>
 * <li>Project dir directory must be placed in dedicated directory e.g. <i>resources/sql</i></li>
 * <li>Migration/Initialization script name must start with a number e.g <i>1.init.sql</i>,
 * this number indicates the subversion of the database migration, e.g. for dir <i>5.0.0</i>
 * and migration script <i>1.init.sql</i> database migration dir will be <i>5.0.0.1</i></li>
 * <li>If a file is not a part of migration it shouldn't end with migration prefix e.g. <i>.sql</i>
 * then resolver will ignore it</li>
 * </ul>
 *
 * <p>For the structure:
 * <pre>
 *   resources/
 *      sql/
 *        5.0.0/
 *          1.init.sql
 *        5.0.0-M1/
 *          1.rename_fields.sql
 *          2.add_workspace_constraint.sql
 *          postgresql/
 *            2.add_workspace_constraint.sql
 *        5.0.1/
 *          1.stacks_migration.sql
 * </pre>
 *
 * And configuration:
 * <pre>
 *     prefix - ""
 *     suffix - ".sql"
 *     separator - "."
 *     locations - "classpath:sql"
 * </pre>
 *
 * <ul>4 database migrations will be resolved
 * <li>5.0.0.1 - initialization script based on file <i>sql/5.0.0/1.init.sql</i></li>
 * <li>5.0.0.1.1 - modification script based on file <i>sql/5.0.0-M1/1.rename_fields.sql</i></li>
 * <li>5.0.0.1.2 - modification script(if postgresql is current provider) based on file
 * <i>sql/5.0.0-M1/postgresql/2.add_workspace_constraint.sql</i></li>
 * <li>5.0.1.1 - modification script based on file <i>sql/5.0.1/1.stacks_migrations.sql</i></li>
 * </ul>
 *
 * <p>It is also possible to configure several locations then all of those locations
 * will be analyzed for migration scripts existence. For example:
 *
 *
 * <p>For the structure:
 * <pre>
 *  che/
 *    resources/
 *       che-schema/
 *         5.0.0/
 *          1.init.sql
 *  another-project/
 *    resources/
 *      custom-schema/
 *        5.0.0/
 *          2.init_additional_tables.sql
 * </pre>
 *
 * And configuration:
 * <pre>
 *     prefix - ""
 *     suffix - ".sql"
 *     separator - "."
 *     locations - "classpath:che-schema, classpath:custom-schema"
 * </pre>
 *
 * <ul>2 database migrations will be resolved
 * <li>5.0.0.1 - initialization script based on file <i>che-schema/5.0.0/1.init.sql</i></li>
 * <li>5.0.0.2 - modification script based on file <i>custom-schema/5.0.0/2.init_additional_tables.sql</i></li>
 * </ul>
 *
 * @author Yevhenii Voevodin
 */
public class CustomSqlMigrationResolver extends BaseMigrationResolver {

    private static final Logger LOG = LoggerFactory.getLogger(CustomSqlMigrationResolver.class);

    private final String vendorName;
    private final ResourcesFinder finder;
    private final VersionResolver versionResolver;
    private final SqlScriptCreator scriptsCreator;
    private final DbSupport dbSupport;
    private final PlaceholderReplacer placeholderReplacer;

    public CustomSqlMigrationResolver(String dbProviderName, DbSupport dbSupport,
            PlaceholderReplacer placeholderReplacer) {
        this.vendorName = dbProviderName;
        this.dbSupport = dbSupport;
        this.placeholderReplacer = placeholderReplacer;
        this.finder = new ResourcesFinder();
        this.versionResolver = new VersionResolver();
        this.scriptsCreator = new SqlScriptCreator();
    }

    @Override
    public Collection<ResolvedMigration> resolveMigrations() {
        try {
            return resolveSqlMigrations();
        } catch (IOException | SQLException x) {
            throw new RuntimeException(x.getLocalizedMessage(), x);
        }
    }

    private List<ResolvedMigration> resolveSqlMigrations() throws IOException, SQLException {
        LOG.info("Searching for sql scripts in locations {}", Arrays.toString(flywayConfiguration.getLocations()));
        final Map<Location, List<Resource>> allResources = finder.findResources(flywayConfiguration);
        LOG.debug("Found scripts: {}", allResources);

        final Map<String, Map<String, SqlScript>> scriptsInDir = new HashMap<>();
        for (Location location : allResources.keySet()) {
            final List<Resource> resources = allResources.get(location);
            for (Resource resource : resources) {
                final SqlScript newScript = scriptsCreator.createScript(location, resource);
                if (!scriptsInDir.containsKey(newScript.dir)) {
                    scriptsInDir.put(newScript.dir, new HashMap<>(4));
                }
                final Map<String, SqlScript> existingScripts = scriptsInDir.get(newScript.dir);
                final SqlScript existingScript = existingScripts.get(newScript.name);
                if (existingScript == null) {
                    existingScripts.put(newScript.name, newScript);
                } else if (Objects.equals(existingScript.vendor, newScript.vendor)) {
                    throw new FlywayException(format(
                            "More than one script with name '%s' is registered for "
                                    + "database vendor '%s', script '%s' conflicts with '%s'",
                            newScript.name, existingScript.vendor, newScript, existingScript));
                } else if (vendorName.equals(newScript.vendor)) {
                    existingScripts.put(newScript.name, newScript);
                }
            }
        }

        final Map<MigrationVersion, ResolvedMigration> migrations = new HashMap<>();
        for (SqlScript script : scriptsInDir.values().stream().flatMap(scripts -> scripts.values().stream())
                .collect(toList())) {
            final ResolvedMigrationImpl migration = new ResolvedMigrationImpl();
            migration.setVersion(versionResolver.resolve(script, flywayConfiguration));
            migration.setScript(script.resource.getLocation());
            migration.setPhysicalLocation(script.resource.getLocationOnDisk());
            migration.setType(MigrationType.SQL);
            migration.setDescription(script.name);
            migration.setChecksum(ByteSource.wrap(script.resource.loadAsBytes()).hash(Hashing.crc32()).asInt());
            migration.setExecutor(new SqlMigrationExecutor(dbSupport, script.resource, placeholderReplacer,
                    flywayConfiguration.getEncoding()));
            if (migrations.put(migration.getVersion(), migration) != null) {
                throw new FlywayException("Two migrations with the same version detected");
            }
        }
        return new ArrayList<>(migrations.values());
    }
}