DefaultClassPathResourceLoader.java
/*
* Copyright 2017-2020 original authors
*
* 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
*
* https://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.
*/
package io.micronaut.core.io.scan;
import io.micronaut.core.io.ResourceLoader;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.util.clhm.ConcurrentLinkedHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.ProviderNotFoundException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.helpers.NOPLogger;
/**
* Loads resources from the classpath.
*
* @author James Kleeh
* @author graemerocher
* @since 1.0
*/
public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
private final Logger log;
private final ClassLoader classLoader;
private final String basePath;
private final URL baseURL;
private final Map<String, Boolean> isDirectoryCache = new ConcurrentLinkedHashMap.Builder<String, Boolean>()
.maximumWeightedCapacity(50).build();
private final boolean missingPath;
private final boolean checkBase;
/**
* Default constructor.
*
* @param classLoader The class loader for loading resources
*/
public DefaultClassPathResourceLoader(ClassLoader classLoader) {
this(classLoader, null);
}
/**
* Use when resources should have a standard base path.
*
* @param classLoader The class loader for loading resources
* @param basePath The path to look for resources under
*/
public DefaultClassPathResourceLoader(ClassLoader classLoader, String basePath) {
this(classLoader, basePath, false);
}
/**
* Use when resources should have a standard base path.
*
* @param classLoader The class loader for loading resources
* @param basePath The path to look for resources under
* @param checkBase If set to {@code true} an extended check for the base path is performed otherwise paths with relative URLs like {@code ../} are prohibited.
*/
public DefaultClassPathResourceLoader(ClassLoader classLoader, String basePath, boolean checkBase) {
this(classLoader, basePath, checkBase, true);
}
/**
* Use when resources should have a standard base path.
*
* @param classLoader The class loader for loading resources
* @param basePath The path to look for resources under
* @param checkBase If set to {@code true} an extended check for the base path is performed otherwise paths with relative URLs like {@code ../} are prohibited.
* @param logEnabled flag to enable or disable logger
*/
public DefaultClassPathResourceLoader(ClassLoader classLoader, String basePath, boolean checkBase, boolean logEnabled) {
log = logEnabled ? LoggerFactory.getLogger(getClass()) : NOPLogger.NOP_LOGGER;
this.classLoader = classLoader;
this.basePath = normalize(basePath);
this.baseURL = checkBase && basePath != null ? classLoader.getResource(normalize(basePath)) : null;
this.missingPath = checkBase && basePath != null && baseURL == null;
this.checkBase = checkBase;
}
/**
* Obtains a resource as a stream.
*
* @param path The path
* @return An optional resource
*/
@Override
public Optional<InputStream> getResourceAsStream(String path) {
if (missingPath) {
return Optional.empty();
} else if (isProhibitedRelativePath(path)) {
return Optional.empty();
}
URL url = classLoader.getResource(prefixPath(path));
if (url != null) {
if (startsWithBase(url)) {
try {
URI uri = url.toURI();
if (uri.getScheme().equals("jar")) {
synchronized (DefaultClassPathResourceLoader.class) {
FileSystem fileSystem = null;
try {
try {
fileSystem = FileSystems.getFileSystem(uri);
} catch (FileSystemNotFoundException e) {
//no-op
}
if (fileSystem == null || !fileSystem.isOpen()) {
try {
fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap(), classLoader);
} catch (FileSystemAlreadyExistsException e) {
fileSystem = FileSystems.getFileSystem(uri);
}
}
Path pathObject = fileSystem.getPath(path);
if (!Files.exists(pathObject) && uri.toString().contains("!/")) {
// Gracefully transform a URL: "jar:file:/{JAR_PATH}!/{PREFIX}!/{RESOURCE}" to path: "{PREFIX}/{RESOURCE}"
final String altPath = Arrays.stream(uri.toString().split("\\!\\/")).skip(1).collect(Collectors.joining("/"));
final Path altPathObject = fileSystem.getPath(altPath);
if (Files.exists(altPathObject) && !Files.isDirectory(pathObject)) {
// Use this path only if the resource exists at that location
pathObject = altPathObject;
}
}
if (Files.isDirectory(pathObject)) {
return Optional.empty();
}
return Optional.of(new ByteArrayInputStream(Files.readAllBytes(pathObject)));
} finally {
if (fileSystem != null && fileSystem.isOpen()) {
try {
fileSystem.close();
} catch (IOException e) {
log.debug("Error shutting down JAR file system [{}]: {}", fileSystem, e.getMessage(), e);
}
}
}
}
} else if (uri.getScheme().equals("file")) {
Path pathObject = Paths.get(uri);
if (Files.isDirectory(pathObject)) {
return Optional.empty();
}
return Optional.of(Files.newInputStream(pathObject));
}
} catch (URISyntaxException | IOException | ProviderNotFoundException e) {
log.debug("Error establishing whether path is a directory: {}", e.getMessage(), e);
}
}
}
// fallback to less sophisticated approach
if (path.indexOf('.') == -1) {
return Optional.empty();
}
final URL u = getResource(path).orElse(null);
if (u != null) {
try {
return Optional.of(u.openStream());
} catch (IOException e) {
// fallback to empty
}
}
return Optional.empty();
}
private boolean startsWithBase(URL url) {
if (checkBase) {
if (baseURL == null) {
return true;
} else {
return url.toExternalForm().startsWith(baseURL.toExternalForm());
}
} else {
return true;
}
}
/**
* Obtains a resource URL.
*
* @param path The path
* @return An optional resource
*/
@Override
public Optional<URL> getResource(String path) {
if (missingPath) {
return Optional.empty();
} else if (isProhibitedRelativePath(path)) {
return Optional.empty();
}
boolean isDirectory = isDirectory(path);
if (!isDirectory) {
URL url = classLoader.getResource(prefixPath(path));
if (url != null && startsWithBase(url)) {
return Optional.of(url);
}
}
return Optional.empty();
}
private boolean isProhibitedRelativePath(String path) {
return !checkBase && path.replace('\\', '/').contains("../");
}
/**
* Obtains a stream of resource URLs.
*
* @param path The path
* @return A resource stream
*/
@Override
public Stream<URL> getResources(String path) {
if (missingPath) {
return Stream.empty();
} else if (isProhibitedRelativePath(path)) {
return Stream.empty();
}
Enumeration<URL> all;
try {
all = classLoader.getResources(prefixPath(path));
} catch (IOException e) {
return Stream.empty();
}
Stream.Builder<URL> builder = Stream.builder();
while (all.hasMoreElements()) {
URL url = all.nextElement();
if (startsWithBase(url)) {
builder.accept(url);
}
}
return builder.build();
}
/**
* @return The class loader used to retrieve resources
*/
@Override
public ClassLoader getClassLoader() {
return classLoader;
}
/**
* @param basePath The path to load resources
* @return The resource loader
*/
@Override
public ResourceLoader forBase(String basePath) {
return new DefaultClassPathResourceLoader(classLoader, basePath);
}
/**
* Need this method to ability disable Slf4J initizalization.
*
* @param basePath The path to load resources
* @param logEnabled flag to enable or disable logger
* @return The resource loader
*/
public ResourceLoader forBase(String basePath, boolean logEnabled) {
return new DefaultClassPathResourceLoader(classLoader, basePath, false, logEnabled);
}
@SuppressWarnings("MagicNumber")
private String normalize(String path) {
if (path != null) {
if (path.startsWith("classpath:")) {
path = path.substring(10);
}
if (path.startsWith("/")) {
path = path.substring(1);
}
if (!path.endsWith("/") && StringUtils.isNotEmpty(path)) {
path = path + "/";
}
}
return path;
}
@SuppressWarnings("ConstantConditions")
private boolean isDirectory(String path) {
return isDirectoryCache.computeIfAbsent(path, s -> {
URL url = classLoader.getResource(prefixPath(path));
if (url != null) {
try {
URI uri = url.toURI();
Path pathObject;
if (uri.getScheme().equals("jar")) {
synchronized (DefaultClassPathResourceLoader.class) {
FileSystem fileSystem = null;
try {
try {
fileSystem = FileSystems.getFileSystem(uri);
} catch (FileSystemNotFoundException e) {
//no-op
}
if (fileSystem == null || !fileSystem.isOpen()) {
fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap(), classLoader);
}
pathObject = fileSystem.getPath(path);
return pathObject == null || Files.isDirectory(pathObject);
} finally {
if (fileSystem != null && fileSystem.isOpen()) {
try {
fileSystem.close();
} catch (IOException e) {
log.debug("Error shutting down JAR file system [{}]: {}", fileSystem, e.getMessage(), e);
}
}
}
}
} else if ("jrt".equals(uri.getScheme())) {
FileSystem fileSystem = null;
try {
fileSystem = FileSystems.getFileSystem(URI.create("jrt:/"));
} catch (FileSystemNotFoundException e) {
//no-op
}
if (fileSystem == null || !fileSystem.isOpen()) {
fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap(), classLoader);
}
pathObject = fileSystem.getPath(path);
return pathObject == null || Files.isDirectory(pathObject);
} else if (uri.getScheme().equals("file")) {
pathObject = Paths.get(uri);
return pathObject == null || Files.isDirectory(pathObject);
}
} catch (URISyntaxException | IOException | ProviderNotFoundException e) {
log.debug("Error establishing whether path is a directory: {}", e.getMessage(), e);
}
}
return path.indexOf('.') == -1; // fallback to less sophisticated approach
});
}
@SuppressWarnings("MagicNumber")
private String prefixPath(String path) {
if (path.startsWith("classpath:")) {
path = path.substring(10);
}
if (basePath != null) {
if (path.startsWith("/")) {
return basePath + path.substring(1);
}
return basePath + path;
}
return path;
}
}