/*
* Copyright 2015-2017 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.junit.platform.commons.util;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.junit.platform.commons.meta.API.Usage.Internal;
import static org.junit.platform.commons.util.BlacklistedExceptions.rethrowIfBlacklisted;
import static org.junit.platform.commons.util.ClassFileVisitor.CLASS_FILE_SUFFIX;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import org.junit.platform.commons.meta.API;
/**
* <h3>DISCLAIMER</h3>
*
* <p>These utilities are intended solely for usage within the JUnit framework
* itself. <strong>Any usage by external parties is not supported.</strong>
* Use at your own risk!
*
* @since 1.0
*/
@API(Internal)
class ClasspathScanner {
private static final Logger LOG = Logger.getLogger(ClasspathScanner.class.getName());
private static final char CLASSPATH_RESOURCE_PATH_SEPARATOR = '/';
private static final char PACKAGE_SEPARATOR_CHAR = '.';
private static final String PACKAGE_SEPARATOR_STRING = String.valueOf(PACKAGE_SEPARATOR_CHAR);
/** Malformed class name InternalError like reported in #401. */
private static final String MALFORMED_CLASS_NAME_ERROR_MESSAGE = "Malformed class name";
private final Supplier<ClassLoader> classLoaderSupplier;
private final BiFunction<String, ClassLoader, Optional<Class<?>>> loadClass;
ClasspathScanner(Supplier<ClassLoader> classLoaderSupplier,
BiFunction<String, ClassLoader, Optional<Class<?>>> loadClass) {
this.classLoaderSupplier = classLoaderSupplier;
this.loadClass = loadClass;
}
List<Class<?>> scanForClassesInPackage(String basePackageName, Predicate<Class<?>> classFilter,
Predicate<String> classNameFilter) {
PackageUtils.assertPackageNameIsValid(basePackageName);
Preconditions.notNull(classFilter, "classFilter must not be null");
Preconditions.notNull(classNameFilter, "classNameFilter must not be null");
basePackageName = basePackageName.trim();
return findClassesForUris(getRootUrisForPackage(basePackageName), basePackageName, classFilter,
classNameFilter);
}
List<Class<?>> scanForClassesInClasspathRoot(URI root, Predicate<Class<?>> classFilter,
Predicate<String> classNameFilter) {
Preconditions.notNull(root, "root must not be null");
Preconditions.notNull(classFilter, "classFilter must not be null");
Preconditions.notNull(classNameFilter, "classNameFilter must not be null");
return findClassesForUri(root, PackageUtils.DEFAULT_PACKAGE_NAME, classFilter, classNameFilter);
}
/**
* Recursively scan for classes in all of the supplied source directories.
*/
private List<Class<?>> findClassesForUris(List<URI> baseUris, String basePackageName,
Predicate<Class<?>> classFilter, Predicate<String> classNameFilter) {
// @formatter:off
return baseUris.stream()
.map(baseUri -> findClassesForUri(baseUri, basePackageName, classFilter, classNameFilter))
.flatMap(Collection::stream)
.distinct()
.collect(toList());
// @formatter:on
}
private List<Class<?>> findClassesForUri(URI baseUri, String basePackageName, Predicate<Class<?>> classFilter,
Predicate<String> classNameFilter) {
try (CloseablePath closeablePath = CloseablePath.create(baseUri)) {
Path baseDir = closeablePath.getPath();
return findClassesForPath(baseDir, basePackageName, classFilter, classNameFilter);
}
catch (PreconditionViolationException ex) {
throw ex;
}
catch (Exception ex) {
logWarning(ex, () -> "Error scanning files for URI " + baseUri);
return emptyList();
}
}
private List<Class<?>> findClassesForPath(Path baseDir, String basePackageName, Predicate<Class<?>> classFilter,
Predicate<String> classNameFilter) {
Preconditions.condition(Files.exists(baseDir), () -> "baseDir must exist: " + baseDir);
List<Class<?>> classes = new ArrayList<>();
try {
Files.walkFileTree(baseDir, new ClassFileVisitor(classFile -> processClassFileSafely(baseDir,
basePackageName, classFilter, classNameFilter, classFile, classes::add)));
}
catch (IOException ex) {
logWarning(ex, () -> "I/O error scanning files in " + baseDir);
}
return classes;
}
private void processClassFileSafely(Path baseDir, String basePackageName, Predicate<Class<?>> classFilter,
Predicate<String> classNameFilter, Path classFile, Consumer<Class<?>> classConsumer) {
Optional<Class<?>> clazz = Optional.empty();
try {
String fullyQualifiedClassName = determineFullyQualifiedClassName(baseDir, basePackageName, classFile);
if (classNameFilter.test(fullyQualifiedClassName)) {
clazz = this.loadClass.apply(fullyQualifiedClassName, getClassLoader());
clazz.filter(classFilter).ifPresent(classConsumer);
}
}
catch (InternalError internalError) {
handleInternalError(classFile, clazz, internalError);
}
catch (Throwable throwable) {
handleThrowable(classFile, throwable);
}
}
private String determineFullyQualifiedClassName(Path baseDir, String basePackageName, Path classFile) {
// @formatter:off
return Stream.of(
basePackageName,
determineSubpackageName(baseDir, classFile),
determineSimpleClassName(classFile)
)
.filter(value -> !value.isEmpty()) // Handle default package appropriately.
.collect(joining(PACKAGE_SEPARATOR_STRING));
// @formatter:on
}
private String determineSimpleClassName(Path classFile) {
String fileName = classFile.getFileName().toString();
return fileName.substring(0, fileName.length() - CLASS_FILE_SUFFIX.length());
}
private String determineSubpackageName(Path baseDir, Path classFile) {
Path relativePath = baseDir.relativize(classFile.getParent());
String pathSeparator = baseDir.getFileSystem().getSeparator();
String subpackageName = relativePath.toString().replace(pathSeparator, PACKAGE_SEPARATOR_STRING);
if (subpackageName.endsWith(pathSeparator)) {
// Workaround for JDK bug: https://bugs.openjdk.java.net/browse/JDK-8153248
subpackageName = subpackageName.substring(0, subpackageName.length() - pathSeparator.length());
}
return subpackageName;
}
private void handleInternalError(Path classFile, Optional<Class<?>> clazz, InternalError ex) {
if (MALFORMED_CLASS_NAME_ERROR_MESSAGE.equals(ex.getMessage())) {
logMalformedClassName(classFile, clazz, ex);
}
else {
logGenericFileProcessingException(classFile, ex);
}
}
private void handleThrowable(Path classFile, Throwable throwable) {
rethrowIfBlacklisted(throwable);
logGenericFileProcessingException(classFile, throwable);
}
private void logMalformedClassName(Path classFile, Optional<Class<?>> clazz, InternalError ex) {
try {
if (clazz.isPresent()) {
// Do not use getSimpleName() or getCanonicalName() here because they will likely
// throw another exception due to the underlying error.
logWarning(ex,
() -> format("The java.lang.Class loaded from path [%s] has a malformed class name [%s].",
classFile.toAbsolutePath(), clazz.get().getName()));
}
else {
logWarning(ex, () -> format("The java.lang.Class loaded from path [%s] has a malformed class name.",
classFile.toAbsolutePath()));
}
}
catch (Throwable t) {
ex.addSuppressed(t);
logGenericFileProcessingException(classFile, ex);
}
}
private void logGenericFileProcessingException(Path classFile, Throwable throwable) {
logWarning(throwable, () -> format("Failed to load java.lang.Class for path [%s] during classpath scanning.",
classFile.toAbsolutePath()));
}
private ClassLoader getClassLoader() {
return this.classLoaderSupplier.get();
}
private static String packagePath(String packageName) {
return packageName.replace(PACKAGE_SEPARATOR_CHAR, CLASSPATH_RESOURCE_PATH_SEPARATOR);
}
private List<URI> getRootUrisForPackage(String basePackageName) {
try {
Enumeration<URL> resources = getClassLoader().getResources(packagePath(basePackageName));
List<URI> uris = new ArrayList<>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
uris.add(resource.toURI());
}
return uris;
}
catch (Exception ex) {
logWarning(ex, () -> "Error reading URIs from class loader for base package " + basePackageName);
return emptyList();
}
}
private static void logWarning(Throwable throwable, Supplier<String> msgSupplier) {
LOG.log(Level.WARNING, throwable, msgSupplier);
}
}