package org.springframework.boot.devtools.restart;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.Supplier;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFile.Kind;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFileURLStreamHandler;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles;
import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles.SourceDirectory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.context.support.ServletContextResourcePatternResolver;
final class ClassLoaderFilesResourcePatternResolver implements ResourcePatternResolver {
private static final String[] LOCATION_PATTERN_PREFIXES = { CLASSPATH_ALL_URL_PREFIX, CLASSPATH_URL_PREFIX };
private static final String WEB_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private final ResourcePatternResolver patternResolverDelegate;
private final PathMatcher antPathMatcher = new AntPathMatcher();
private final ClassLoaderFiles classLoaderFiles;
ClassLoaderFilesResourcePatternResolver(AbstractApplicationContext applicationContext,
ClassLoaderFiles classLoaderFiles) {
this.classLoaderFiles = classLoaderFiles;
this.patternResolverDelegate = getResourcePatternResolverFactory()
.getResourcePatternResolver(applicationContext, retrieveResourceLoader(applicationContext));
}
private ResourceLoader retrieveResourceLoader(ApplicationContext applicationContext) {
Field field = ReflectionUtils.findField(applicationContext.getClass(), "resourceLoader", ResourceLoader.class);
if (field == null) {
return null;
}
ReflectionUtils.makeAccessible(field);
return (ResourceLoader) ReflectionUtils.getField(field, applicationContext);
}
private ResourcePatternResolverFactory getResourcePatternResolverFactory() {
if (ClassUtils.isPresent(WEB_CONTEXT_CLASS, null)) {
return new WebResourcePatternResolverFactory();
}
return new ResourcePatternResolverFactory();
}
@Override
public ClassLoader getClassLoader() {
return this.patternResolverDelegate.getClassLoader();
}
@Override
public Resource getResource(String location) {
Resource candidate = this.patternResolverDelegate.getResource(location);
if (isDeleted(candidate)) {
return new DeletedClassLoaderFileResource(location);
}
return candidate;
}
@Override
public Resource[] getResources(String locationPattern) throws IOException {
List<Resource> resources = new ArrayList<>();
Resource[] candidates = this.patternResolverDelegate.getResources(locationPattern);
for (Resource candidate : candidates) {
if (!isDeleted(candidate)) {
resources.add(candidate);
}
}
resources.addAll(getAdditionalResources(locationPattern));
return resources.toArray(new Resource[0]);
}
private List<Resource> getAdditionalResources(String locationPattern) throws MalformedURLException {
List<Resource> additionalResources = new ArrayList<>();
String trimmedLocationPattern = trimLocationPattern(locationPattern);
for (SourceDirectory sourceDirectory : this.classLoaderFiles.getSourceDirectories()) {
for (Entry<String, ClassLoaderFile> entry : sourceDirectory.getFilesEntrySet()) {
String name = entry.getKey();
ClassLoaderFile file = entry.getValue();
if (file.getKind() != Kind.DELETED && this.antPathMatcher.match(trimmedLocationPattern, name)) {
URL url = new URL("reloaded", null, -1, "/" + name, new ClassLoaderFileURLStreamHandler(file));
UrlResource resource = new UrlResource(url);
additionalResources.add(resource);
}
}
}
return additionalResources;
}
private String trimLocationPattern(String pattern) {
for (String prefix : LOCATION_PATTERN_PREFIXES) {
if (pattern.startsWith(prefix)) {
return pattern.substring(prefix.length());
}
}
return pattern;
}
private boolean isDeleted(Resource resource) {
for (SourceDirectory sourceDirectory : this.classLoaderFiles.getSourceDirectories()) {
for (Entry<String, ClassLoaderFile> entry : sourceDirectory.getFilesEntrySet()) {
try {
String name = entry.getKey();
ClassLoaderFile file = entry.getValue();
if (file.getKind() == Kind.DELETED && resource.exists()
&& resource.getURI().toString().endsWith(name)) {
return true;
}
}
catch (IOException ex) {
throw new IllegalStateException("Failed to retrieve URI from '" + resource + "'", ex);
}
}
}
return false;
}
static final class DeletedClassLoaderFileResource extends AbstractResource {
private final String name;
private DeletedClassLoaderFileResource(String name) {
this.name = name;
}
@Override
public boolean exists() {
return false;
}
@Override
public String getDescription() {
return "Deleted: " + this.name;
}
@Override
public InputStream getInputStream() throws IOException {
throw new IOException(this.name + " has been deleted");
}
}
private static class ResourcePatternResolverFactory {
ResourcePatternResolver getResourcePatternResolver(AbstractApplicationContext applicationContext,
ResourceLoader resourceLoader) {
ResourceLoader targetResourceLoader = (resourceLoader != null) ? resourceLoader
: new ApplicationContextResourceLoader(applicationContext::getProtocolResolvers);
return new PathMatchingResourcePatternResolver(targetResourceLoader);
}
}
private static class WebResourcePatternResolverFactory extends ResourcePatternResolverFactory {
@Override
public ResourcePatternResolver getResourcePatternResolver(AbstractApplicationContext applicationContext,
ResourceLoader resourceLoader) {
if (applicationContext instanceof WebApplicationContext) {
return getServletContextResourcePatternResolver(applicationContext, resourceLoader);
}
return super.getResourcePatternResolver(applicationContext, resourceLoader);
}
private ResourcePatternResolver getServletContextResourcePatternResolver(
AbstractApplicationContext applicationContext, ResourceLoader resourceLoader) {
ResourceLoader targetResourceLoader = (resourceLoader != null) ? resourceLoader
: new WebApplicationContextResourceLoader(applicationContext::getProtocolResolvers,
(WebApplicationContext) applicationContext);
return new ServletContextResourcePatternResolver(targetResourceLoader);
}
}
private static class ApplicationContextResourceLoader extends DefaultResourceLoader {
private final Supplier<Collection<ProtocolResolver>> protocolResolvers;
ApplicationContextResourceLoader(Supplier<Collection<ProtocolResolver>> protocolResolvers) {
super(null);
this.protocolResolvers = protocolResolvers;
}
@Override
public Collection<ProtocolResolver> getProtocolResolvers() {
return this.protocolResolvers.get();
}
}
private static class WebApplicationContextResourceLoader extends ApplicationContextResourceLoader {
private final WebApplicationContext applicationContext;
WebApplicationContextResourceLoader(Supplier<Collection<ProtocolResolver>> protocolResolvers,
WebApplicationContext applicationContext) {
super(protocolResolvers);
this.applicationContext = applicationContext;
}
@Override
protected Resource getResourceByPath(String path) {
if (this.applicationContext.getServletContext() != null) {
return new ServletContextResource(this.applicationContext.getServletContext(), path);
}
return super.getResourceByPath(path);
}
}
}