MigrationScannerManager.java

/*-
 * ========================LICENSE_START=================================
 * flyway-nc-core
 * ========================================================================
 * Copyright (C) 2010 - 2025 Red Gate Software Ltd
 * ========================================================================
 * 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.
 * =========================LICENSE_END==================================
 */
package org.flywaydb.nc.migration;

import static org.flywaydb.core.api.resource.LoadableResource.createPlaceholderReplacingLoadableResource;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.HashSet;
import java.util.Objects;
import lombok.CustomLog;
import org.flywaydb.core.api.CoreErrorCode;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.Location;
import org.flywaydb.core.api.configuration.Configuration;
import org.flywaydb.core.api.resource.LoadableResource;
import org.flywaydb.core.api.resource.LoadableResourceMetadata;
import org.flywaydb.core.internal.nc.NativeConnectorsMigrationScanner;
import org.flywaydb.core.internal.parser.ParsingContext;
import org.flywaydb.core.internal.resolver.ChecksumCalculator;
import org.flywaydb.core.internal.resource.ResourceName;
import org.flywaydb.core.internal.resource.ResourceNameParser;
import org.flywaydb.core.internal.sqlscript.SqlScriptMetadata;
import org.flywaydb.core.internal.util.Pair;

@CustomLog
public class MigrationScannerManager {
    private final List<? extends NativeConnectorsMigrationScanner> scanners;

    public MigrationScannerManager(final Configuration configuration) {
        this(configuration.getPluginRegister().getPlugins(NativeConnectorsMigrationScanner.class));
    }
    
    public MigrationScannerManager(final List<? extends NativeConnectorsMigrationScanner> scanners) {
        this.scanners = scanners;
    }

    public Collection<LoadableResourceMetadata> scan(final Configuration configuration, final ParsingContext parsingContext) {
        if (scanners.isEmpty()){
            LOG.warn("Native Connectors Mode is set but no migrations scanners loaded");
            return Collections.emptyList();
        }

        final List<LoadableResourceMetadata> resources = Arrays.stream(configuration.getLocations())
                                                               .flatMap(location -> scan(location,configuration, parsingContext).stream())
                                                               .map(resource -> getLoadableResourceMetadata(resource, configuration, parsingContext))
                                                               .toList();

        final Collection<LoadableResourceMetadata> resourceSet = new HashSet<>();
        resources.forEach(resource -> {
            if(resourceSet.contains(resource)) {
                final LoadableResourceMetadata first = resourceSet.stream()
                                                                  .filter(loadableResourceMetadata ->
                                                                              loadableResourceMetadata.equals(resource))
                                                                  .findFirst()
                                                                  .get();
                if (first.version() != null) {
                    throw new FlywayException(String.format(
                        "Found more than one migration with version %s\nOffenders:\n-> %s \n-> %s",
                        resource.version(),
                        resource.loadableResource().getAbsolutePath(),
                        first.loadableResource().getAbsolutePath()), CoreErrorCode.DUPLICATE_VERSIONED_MIGRATION);
                } else {
                    throw new FlywayException(String.format("Found more than one repeatable migration with description '%s'\nOffenders:\n-> %s \n-> %s ",
                                                            resource.description(),
                                                            resource.loadableResource().getAbsolutePath(),
                                                            first.loadableResource().getAbsolutePath()), CoreErrorCode.DUPLICATE_REPEATABLE_MIGRATION);
                }
            } else {
                resourceSet.add(resource);
            }
        });
        
        return resources;              
    }

    private static LoadableResourceMetadata getLoadableResourceMetadata(final Pair<LoadableResource, SqlScriptMetadata> resource,
        final Configuration configuration,
        final ParsingContext parsingContext) {
        
        final ResourceNameParser resourceNameParser = new ResourceNameParser(configuration);
        final ResourceName resourceName = resourceNameParser.parse(resource.getLeft().getFilename());

        SqlScriptMetadata sqlScriptMetadata = resource.getRight();
        boolean placeholderReplacement = (sqlScriptMetadata == null
            || sqlScriptMetadata.placeholderReplacement() == null)
            ? configuration.isPlaceholderReplacement()
            : sqlScriptMetadata.placeholderReplacement();

        final int checksum = getChecksumForLoadableResource(
            Objects.equals(resourceName.getPrefix(), configuration.getRepeatableSqlMigrationPrefix()),
            placeholderReplacement,
            resource.getLeft(),
            resourceName,
            configuration,
            parsingContext);
        return new LoadableResourceMetadata(
            resourceName.getVersion(),
            resourceName.getDescription(),
            resourceName.getPrefix(),
            placeholderReplacement ? createPlaceholderReplacingLoadableResource(resource.getLeft(), configuration, parsingContext) : resource.getLeft(),
            resource.getRight(),
            checksum, 
            null);
    }

    private static Integer getChecksumForLoadableResource(
        final boolean repeatable,
        final boolean placeholderReplacement,
        final LoadableResource resource,
        final ResourceName resourceName,
        final Configuration configuration,
        final ParsingContext parsingContext) {
        if (repeatable && placeholderReplacement) {
            parsingContext.updateFilenamePlaceholder(resourceName, configuration);
            return ChecksumCalculator.calculate(createPlaceholderReplacingLoadableResource(resource, configuration, parsingContext));
        }
        return ChecksumCalculator.calculate(resource);
    }    

    private Collection<Pair<LoadableResource, SqlScriptMetadata>> scan(final Location location, final Configuration configuration, final ParsingContext parsingContext) {
        return scanners.stream()
                       .flatMap(scanner -> scanner.scan(location, configuration, parsingContext).stream())
                       .toList();
    }
}