diff --git a/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/NativeImageResourceProvider.java b/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/NativeImageResourceProvider.java index d5065b9f689f..f5a7bdbc4e60 100644 --- a/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/NativeImageResourceProvider.java +++ b/module/spring-boot-flyway/src/main/java/org/springframework/boot/flyway/autoconfigure/NativeImageResourceProvider.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.net.URI; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; @@ -46,6 +47,7 @@ * {@link PathMatchingResourcePatternResolver} to find migration files in a native image. * * @author Moritz Halbritter + * @author Dongliang Xie */ class NativeImageResourceProvider implements ResourceProvider { @@ -106,9 +108,8 @@ public Collection getResources(String prefix, String[] suffixe } private ClassPathResource asClassPathResource(LocatedResource locatedResource) { - Location location = locatedResource.location(); - String fileNameWithAbsolutePath = location.getRootPath() + "/" + locatedResource.resource().getFilename(); - return new ClassPathResource(location, fileNameWithAbsolutePath, this.classLoader, this.encoding); + return new ClassPathResource(locatedResource.location(), locatedResource.path(), this.classLoader, + this.encoding); } private void ensureInitialized() { @@ -140,11 +141,60 @@ private void initialize() { } Resource[] resources = getResources(resolver, location, root); for (Resource resource : resources) { - this.locatedResources.add(new LocatedResource(resource, location)); + this.locatedResources + .add(new LocatedResource(resource, location, getClassPathResourcePath(location, root, resource))); } } } + private String getClassPathResourcePath(Location location, Resource root, Resource resource) { + if (resource instanceof org.springframework.core.io.ClassPathResource classPathResource) { + return classPathResource.getPath(); + } + String rootPath = location.getRootPath(); + String resourcePath = getResourcePathRelativeToRoot(root, resource, rootPath); + return (rootPath.isEmpty()) ? resourcePath : rootPath + "/" + resourcePath; + } + + private String getResourcePathRelativeToRoot(Resource root, Resource resource, String rootPath) { + try { + URI rootUri = root.getURI(); + URI resourceUri = resource.getURI(); + String relativePath = getRelativePath(rootUri, resourceUri); + if (relativePath != null) { + return relativePath; + } + String path = getUriPath(resourceUri); + if (!rootPath.isEmpty()) { + int rootPathIndex = path.indexOf(rootPath + "/"); + if (rootPathIndex != -1) { + return path.substring(rootPathIndex + rootPath.length() + 1); + } + } + String filename = resource.getFilename(); + return (filename != null) ? filename : path; + } + catch (IOException ex) { + throw new UncheckedIOException("Failed to determine path for " + resource, ex); + } + } + + private @Nullable String getRelativePath(URI rootUri, URI resourceUri) { + String rootPath = asDirectoryPath(rootUri); + String resourcePath = getUriPath(resourceUri); + return (resourcePath.startsWith(rootPath)) ? resourcePath.substring(rootPath.length()) : null; + } + + private String asDirectoryPath(URI uri) { + String path = getUriPath(uri); + return (path.endsWith("/")) ? path : path + "/"; + } + + private String getUriPath(URI uri) { + String path = uri.getPath(); + return (path != null) ? path : uri.toString(); + } + private Resource[] getResources(PathMatchingResourcePatternResolver resolver, Location location, Resource root) { try { return resolver.getResources(root.getURI() + "/**/*"); @@ -154,7 +204,7 @@ private Resource[] getResources(PathMatchingResourcePatternResolver resolver, Lo } } - private record LocatedResource(Resource resource, Location location) { + private record LocatedResource(Resource resource, Location location, String path) { } diff --git a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/NativeImageResourceProviderCustomizerTests.java b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/NativeImageResourceProviderCustomizerTests.java index 541f2d0215bb..9c2ac033b3fb 100644 --- a/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/NativeImageResourceProviderCustomizerTests.java +++ b/module/spring-boot-flyway/src/test/java/org/springframework/boot/flyway/autoconfigure/NativeImageResourceProviderCustomizerTests.java @@ -16,23 +16,34 @@ package org.springframework.boot.flyway.autoconfigure; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.flywaydb.core.api.Location; import org.flywaydb.core.api.ResourceProvider; import org.flywaydb.core.api.configuration.FluentConfiguration; import org.flywaydb.core.api.resource.LoadableResource; import org.flywaydb.core.internal.resource.NoopResourceProvider; +import org.flywaydb.core.internal.scanner.Scanner; import org.junit.jupiter.api.Test; +import org.springframework.boot.testsupport.classpath.ForkedClassPath; import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.boot.testsupport.classpath.resources.WithResources; +import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link NativeImageResourceProviderCustomizer}. * * @author Moritz Halbritter + * @author Dongliang Xie */ class NativeImageResourceProviderCustomizerTests { @@ -70,6 +81,29 @@ void nativeImageResourceProviderShouldFindNestedMigrations() { assertThat(migrations).containsExactlyInAnyOrder(v1, v2); } + @Test + @ForkedClassPath + @WithResource(name = "db/migration/nested/V2__users.sql", content = "select 1;") + void nativeImageResourceProviderShouldReadNestedMigrations() throws IOException { + System.setProperty("org.graalvm.nativeimage.imagecode", "true"); + try { + @SuppressWarnings("unchecked") + Scanner scanner = mock(Scanner.class); + given(scanner.getResources("V", ".sql")).willReturn(Collections.emptyList()); + Location location = Location.fromPath("classpath:", "db/migration"); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + ResourceProvider resourceProvider = new NativeImageResourceProvider(scanner, classLoader, List.of(location), + StandardCharsets.UTF_8, true); + Collection migrations = resourceProvider.getResources("V", new String[] { ".sql" }); + assertThat(migrations).hasSize(1); + LoadableResource migration = migrations.iterator().next(); + assertThat(FileCopyUtils.copyToString(migration.read())).isEqualTo("select 1;"); + } + finally { + System.clearProperty("org.graalvm.nativeimage.imagecode"); + } + } + @Test void shouldBackOffOnCustomResourceProvider() { FluentConfiguration configuration = new FluentConfiguration();