diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/QuarkusPathLocationScanner.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/QuarkusPathLocationScanner.java index 6482fb2248611..bd4807585caa7 100644 --- a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/QuarkusPathLocationScanner.java +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/QuarkusPathLocationScanner.java @@ -1,42 +1,141 @@ package io.quarkus.flyway.runtime; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import org.flywaydb.core.api.Location; import org.flywaydb.core.api.logging.Log; import org.flywaydb.core.api.logging.LogFactory; import org.flywaydb.core.internal.resource.LoadableResource; +import org.flywaydb.core.internal.resource.classpath.ClassPathResource; import org.flywaydb.core.internal.scanner.classpath.ResourceAndClassScanner; public final class QuarkusPathLocationScanner implements ResourceAndClassScanner { - private static final Log LOG = LogFactory.getLog( QuarkusPathLocationScanner.class ); - private final Location location; - - public QuarkusPathLocationScanner(Location location) { - this.location = location; - LOG.warn( "LOCATION " + location ); - } - - /** - * Scans the classpath for resources under the configured location. - * - * @return The resources that were found. - */ - @Override - public Collection scanForResources() { - // NEED TO DO SOMETHING TO RETURN THE RESOURCES ADDED TO SVM in FlywayBuildStep - return Collections.emptyList(); - } - - /** - * Scans the classpath for concrete classes under the specified package implementing this interface. - * Non-instantiable abstract classes are filtered out. - * - * @return The non-abstract classes that were found. - */ - @Override - public Collection> scanForClasses() { - return Collections.emptyList(); - } + private static final Log LOG = LogFactory.getLog(QuarkusPathLocationScanner.class); + // TODO: this should be configurable, using flyway default + private final static String DEFAULT_NATIVE_LOCATION = "db/migration"; + private static Set ALL_SQL_RESOURCES; + + static { + try { + ALL_SQL_RESOURCES = discoverApplicationMigrations(); + } catch (IOException | URISyntaxException e) { + throw new IllegalStateException("Could not discover Flyway migrations", e); + } + } + + public QuarkusPathLocationScanner(Location location) { + if (!location.getPath().equals(DEFAULT_NATIVE_LOCATION)) { + LOG.error("Invalid migrations location: " + location.getPath() + ". Flyway migrations must be located in " + + DEFAULT_NATIVE_LOCATION); + } + } + + /** + * Scans the classpath for resources under the configured DEFAULT_NATIVE_LOCATION. + * + * @return The resources that were found. + */ + @Override + public Collection scanForResources() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Set resources = new HashSet<>(); + Location defaultLocation = new Location(DEFAULT_NATIVE_LOCATION); + for (String file : ALL_SQL_RESOURCES) { + LOG.info("Loading " + file); + resources.add(new ClassPathResource(defaultLocation, file, classLoader, StandardCharsets.UTF_8)); + } + return resources; + } + + private static Set discoverApplicationMigrations() throws IOException, URISyntaxException { + Set resources = new HashSet<>(); + try { + Enumeration migrations = Thread.currentThread().getContextClassLoader().getResources( + DEFAULT_NATIVE_LOCATION); + while (migrations.hasMoreElements()) { + URL path = migrations.nextElement(); + LOG.info("Adding application migrations in path: " + path); + if ("jar".equals(path.getProtocol())) { + resources = scanInJar(path); + } else { + throw new IllegalStateException("Expecting jar file. Protocol not supported:" + path.getProtocol()); + } + + } + return resources; + } catch (IOException e) { + LOG.error("Error discovering application migrations: " + e.getMessage(), e); + throw e; + } + } + + /** + * Scans the classpath for concrete classes under the specified package implementing this interface. + * Non-instantiable abstract classes are filtered out. + * + * @return The non-abstract classes that were found. + */ + @Override + public Collection> scanForClasses() { + // Classes are not supported in native mode + return Collections.emptyList(); + } + + public static Set scanInJar(URL locationUrl) { + JarFile jarFile; + try { + jarFile = getJarFromUrl(locationUrl); + } catch (IOException e) { + LOG.warn("Unable to determine jar from url (" + locationUrl + "): " + e.getMessage()); + return Collections.emptySet(); + } + + try { + return findResourceNamesFromJarFile(jarFile); + } finally { + try { + jarFile.close(); + } catch (IOException e) { + // Ignore + } + } + } + + private static JarFile getJarFromUrl(URL locationUrl) throws IOException { + URLConnection con = locationUrl.openConnection(); + if (con instanceof JarURLConnection) { + // Should usually be the case for traditional JAR files. + JarURLConnection jarCon = (JarURLConnection) con; + jarCon.setUseCaches(false); + return jarCon.getJarFile(); + } + throw new IllegalStateException("Could not open the jar file " + locationUrl); + } + + private static Set findResourceNamesFromJarFile(JarFile jarFile) { + Set resourceNames = new TreeSet<>(); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + String entryName = entries.nextElement().getName(); + if (entryName.startsWith(DEFAULT_NATIVE_LOCATION) && !entryName.endsWith("/")) { + LOG.info("Discovered " + entryName); + resourceNames.add(entryName); + } + } + + return resourceNames; + } }