ClassPathScanner.java
/*-
* ========================LICENSE_START=================================
* flyway-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.core.internal.scanner.classpath;
import lombok.CustomLog;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.Location;
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.resource.LoadableResource;
import org.flywaydb.core.internal.resource.classpath.ClassPathResource;
import org.flywaydb.core.internal.scanner.LocationScannerCache;
import org.flywaydb.core.internal.scanner.ResourceNameCache;
import org.flywaydb.core.internal.scanner.classpath.jboss.JBossVFSv2UrlResolver;
import org.flywaydb.core.internal.scanner.classpath.jboss.JBossVFSv3ClassPathLocationScanner;
import org.flywaydb.core.internal.util.*;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
@CustomLog
public class ClassPathScanner<I> implements ResourceAndClassScanner<I> {
private final Class<I> implementedInterface;
private final ClassLoader classLoader;
private final Location location;
private final Set<LoadableResource> resources = new HashSet<>();
/**
* Cache location lookups.
*/
private final Map<Location, List<URL>> locationUrlCache = new HashMap<>();
/**
* Cache location scanners.
*/
private final LocationScannerCache locationScannerCache;
/**
* Cache resource names.
*/
private final ResourceNameCache resourceNameCache;
/**
* Whether to throw an exception if a location was not found.
*/
private final boolean throwOnMissingLocations;
public ClassPathScanner(Class<I> implementedInterface, ClassLoader classLoader, Charset encoding, Location location,
ResourceNameCache resourceNameCache,
LocationScannerCache locationScannerCache,
boolean throwOnMissingLocations,
boolean stream) {
this.implementedInterface = implementedInterface;
this.classLoader = classLoader;
this.location = location;
this.resourceNameCache = resourceNameCache;
this.locationScannerCache = locationScannerCache;
this.throwOnMissingLocations = throwOnMissingLocations;
LOG.debug("Scanning for classpath resources at '" + location + "' ...");
for (Pair<String, String> resourceNameAndParentURL : findResourceNamesAndParentURLs()) {
String resourceName = resourceNameAndParentURL.getLeft();
String parentURL = resourceNameAndParentURL.getRight();
resources.add(new ClassPathResource(location, resourceName, classLoader, encoding, parentURL, stream));
LOG.debug("Found resource: " + resourceNameAndParentURL.getLeft());
}
}
@Override
public Collection<LoadableResource> scanForResources() {
return resources;
}
@Override
public Collection<Class<? extends I>> scanForClasses() {
LOG.debug("Scanning for classes at " + location);
List<Class<? extends I>> classes = new ArrayList<>();
for (LoadableResource resource : resources) {
if (resource.getAbsolutePath().endsWith(".class")) {
Class<? extends I> clazz;
try {
clazz = ClassUtils.loadClass(
implementedInterface,
toClassName(resource.getAbsolutePath()),
classLoader);
} catch(Throwable e) {
Throwable rootCause = ExceptionUtils.getRootCause(e);
LOG.warn("Skipping " + Callback.class + ": " + ClassUtils.formatThrowable(e) + (
rootCause == e
? ""
: " caused by " + ClassUtils.formatThrowable(rootCause)
+ " at " + ExceptionUtils.getThrowLocation(rootCause)
));
clazz = null;
}
if (clazz != null) {
classes.add(clazz);
}
}
}
return classes;
}
/**
* Converts this resource name to a fully qualified class name.
*
* @param resourceName The resource name.
* @return The class name.
*/
private String toClassName(String resourceName) {
String nameWithDots = resourceName.replace("/", ".");
return nameWithDots.substring(0, (nameWithDots.length() - ".class".length()));
}
private Set<Pair<String, String>> findResourceNamesAndParentURLs() {
Set<Pair<String, String>> resourceNamesAndParentURLs = new TreeSet<>();
List<URL> locationUrls = getLocationUrlsForPath(location);
for (URL locationUrl : locationUrls) {
LOG.debug("Scanning URL: " + locationUrl.toExternalForm());
UrlResolver urlResolver = createUrlResolver(locationUrl.getProtocol());
URL resolvedUrl = urlResolver.toStandardJavaUrl(locationUrl);
String protocol = resolvedUrl.getProtocol();
ClassPathLocationScanner classPathLocationScanner = createLocationScanner(protocol);
if (classPathLocationScanner == null) {
String scanRoot = UrlUtils.toFilePath(resolvedUrl);
LOG.warn("Unable to scan location: " + scanRoot + " (unsupported protocol: " + protocol + ")");
} else {
Set<String> names = resourceNameCache.get(classPathLocationScanner, resolvedUrl);
if (names == null) {
names = classPathLocationScanner.findResourceNames(location.getRootPath(), resolvedUrl);
resourceNameCache.put(classPathLocationScanner, resolvedUrl, names);
}
Set<String> filteredNames = new HashSet<>();
for (String name : names) {
if (location.matchesPath(name)) {
filteredNames.add(name);
}
}
for (String filteredName : filteredNames) {
resourceNamesAndParentURLs.add(Pair.of(filteredName, resolvedUrl.getPath()));
}
}
}
// Make an additional attempt at finding resources in jar files in case the URL scanning method above didn't
// yield any results.
boolean locationResolved = !locationUrls.isEmpty();
// Starting with Java 11, resources at the root of the classpath aren't being found using the URL scanning
// method above and we need to revert to Jar file walking.
boolean isClassPathRoot = location.isClassPath() && "".equals(location.getRootPath());
if (!locationResolved || isClassPathRoot) {
if (classLoader instanceof URLClassLoader) {
URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
for (URL url : urlClassLoader.getURLs()) {
if ("file".equals(url.getProtocol())
&& url.getPath().endsWith(".jar")
&& !url.getPath().matches(".*" + Pattern.quote("/jre/lib/") + ".*")) {
// All non-system jars on disk
JarFile jarFile;
try {
try {
jarFile = new JarFile(url.toURI().getSchemeSpecificPart());
} catch (URISyntaxException ex) {
// Fallback for URLs that are not valid URIs (should hardly ever happen).
jarFile = new JarFile(url.getPath().substring("file:".length()));
}
} catch (IOException | SecurityException e) {
LOG.warn("Skipping unloadable jar file: " + url + " (" + e.getMessage() + ")");
continue;
}
try {
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
String entryName = entries.nextElement().getName();
if (entryName.startsWith(location.getRootPath())) {
locationResolved = true;
resourceNamesAndParentURLs.add(Pair.of(entryName, url.getPath()));
}
}
} finally {
try {
jarFile.close();
} catch (IOException e) {
// Ignore
}
}
}
}
}
}
if (!locationResolved) {
String message = "Unable to resolve location " + location + ".";
if (throwOnMissingLocations) {
throw new FlywayException(message);
} else {
LOG.debug(message);
}
}
return resourceNamesAndParentURLs;
}
/**
* Gets the physical location urls for this logical path on the classpath.
*
* @param location The location on the classpath.
* @return The underlying physical URLs.
*/
private List<URL> getLocationUrlsForPath(Location location) {
if (locationUrlCache.containsKey(location)) {
return locationUrlCache.get(location);
}
LOG.debug("Determining location urls for " + location + " using ClassLoader " + classLoader + " ...");
List<URL> locationUrls = new ArrayList<>();
if (classLoader.getClass().getName().startsWith("com.ibm")) {
// WebSphere
Enumeration<URL> urls;
try {
urls = classLoader.getResources(location.getRootPath() + "/flyway.location");
if (!urls.hasMoreElements()) {
LOG.error("Unable to resolve location " + location + " (ClassLoader: " + classLoader + ")"
+ " On WebSphere an empty file named flyway.location must be present on the classpath location for WebSphere to find it!");
}
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
locationUrls.add(new URL(UrlUtils.decodeURL(url.toExternalForm()).replace("/flyway.location", "")));
}
} catch (IOException e) {
LOG.error("Unable to resolve location " + location + " (ClassLoader: " + classLoader + ")"
+ " On WebSphere an empty file named flyway.location must be present on the classpath location for WebSphere to find it!");
}
} else {
Enumeration<URL> urls;
try {
urls = classLoader.getResources(location.getRootPath());
while (urls.hasMoreElements()) {
locationUrls.add(urls.nextElement());
}
} catch (IOException e) {
LOG.error("Unable to resolve location " + location + " (ClassLoader: " + classLoader + "): " + e.getMessage() + ".");
}
}
locationUrlCache.put(location, locationUrls);
return locationUrls;
}
/**
* Creates an appropriate URL resolver scanner for this url protocol.
*
* @param protocol The protocol of the location url to scan.
* @return The url resolver for this protocol.
*/
private UrlResolver createUrlResolver(String protocol) {
if (protocol.startsWith("vfs") && new FeatureDetector(classLoader).isJBossVFSv2Available()) {
return new JBossVFSv2UrlResolver();
}
return new DefaultUrlResolver();
}
/**
* Creates an appropriate location scanner for this url protocol.
*
* @param protocol The protocol of the location url to scan.
* @return The location scanner or {@code null} if it could not be created.
*/
private ClassPathLocationScanner createLocationScanner(String protocol) {
if (locationScannerCache.containsKey(protocol)) {
return locationScannerCache.get(protocol);
}
if ("file".equals(protocol)) {
FileSystemClassPathLocationScanner locationScanner = new FileSystemClassPathLocationScanner();
locationScannerCache.put(protocol, locationScanner);
resourceNameCache.put(locationScanner, new HashMap<>());
return locationScanner;
}
if ("jar".equals(protocol) || isTomcat(protocol) || isWebLogic(protocol) || isWebSphere(protocol)) {
String separator = isTomcat(protocol) ? "*/" : "!/";
ClassPathLocationScanner locationScanner = new JarFileClassPathLocationScanner(separator);
locationScannerCache.put(protocol, locationScanner);
resourceNameCache.put(locationScanner, new HashMap<>());
return locationScanner;
}
FeatureDetector featureDetector = new FeatureDetector(classLoader);
if ("vfs".equals(protocol) && featureDetector.isJBossVFSv3Available()) {
JBossVFSv3ClassPathLocationScanner locationScanner = new JBossVFSv3ClassPathLocationScanner();
locationScannerCache.put(protocol, locationScanner);
resourceNameCache.put(locationScanner, new HashMap<>());
return locationScanner;
}
if ((isFelix(protocol) || isEquinox(protocol)) && featureDetector.isOsgiFrameworkAvailable()) {
OsgiClassPathLocationScanner locationScanner = new OsgiClassPathLocationScanner();
locationScannerCache.put(protocol, locationScanner);
resourceNameCache.put(locationScanner, new HashMap<>());
return locationScanner;
}
return null;
}
private boolean isEquinox(String protocol) {
return "bundleresource".equals(protocol);
}
private boolean isFelix(String protocol) {
return "bundle".equals(protocol);
}
private boolean isWebSphere(String protocol) {
return "wsjar".equals(protocol);
}
private boolean isWebLogic(String protocol) {
return "zip".equals(protocol);
}
private boolean isTomcat(String protocol) {
return "war".equals(protocol);
}
}