Skip to content

Commit 791cb4d

Browse files
committed
Polish ClasspathScanner
Issue: #401
1 parent 65e91b4 commit 791cb4d

File tree

3 files changed

+121
-109
lines changed

3 files changed

+121
-109
lines changed

junit-platform-commons/src/main/java/org/junit/platform/commons/util/ClasspathScanner.java

Lines changed: 87 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import static org.junit.platform.commons.util.BlacklistedExceptions.rethrowIfBlacklisted;
1616

1717
import java.io.File;
18-
import java.io.IOException;
1918
import java.net.URL;
2019
import java.util.ArrayList;
2120
import java.util.Collections;
@@ -25,6 +24,7 @@
2524
import java.util.function.BiFunction;
2625
import java.util.function.Predicate;
2726
import java.util.function.Supplier;
27+
import java.util.logging.Level;
2828
import java.util.logging.Logger;
2929

3030
import org.junit.platform.commons.meta.API;
@@ -45,167 +45,182 @@ class ClasspathScanner {
4545

4646
private static final String CLASS_FILE_SUFFIX = ".class";
4747

48+
private static final String ROOT_PACKAGE_NAME = "";
49+
50+
/** Malformed class name InternalError like reported in #401. */
51+
private static final String MALFORMED_CLASS_NAME_ERROR_MESSAGE = "Malformed class name";
52+
4853
private final Supplier<ClassLoader> classLoaderSupplier;
4954

5055
private final BiFunction<String, ClassLoader, Optional<Class<?>>> loadClass;
5156

5257
ClasspathScanner(Supplier<ClassLoader> classLoaderSupplier,
5358
BiFunction<String, ClassLoader, Optional<Class<?>>> loadClass) {
59+
5460
this.classLoaderSupplier = classLoaderSupplier;
5561
this.loadClass = loadClass;
5662
}
5763

5864
boolean isPackage(String packageName) {
5965
Preconditions.notBlank(packageName, "package name must not be null or blank");
6066

61-
String path = packagePath(packageName);
6267
try {
63-
Enumeration<URL> resource = classLoaderSupplier.get().getResources(path);
64-
return resource.hasMoreElements();
68+
return getClassLoader().getResources(packagePath(packageName)).hasMoreElements();
6569
}
66-
catch (IOException e) {
70+
catch (Exception ex) {
6771
return false;
6872
}
6973
}
7074

7175
List<Class<?>> scanForClassesInPackage(String basePackageName, Predicate<Class<?>> classFilter) {
72-
Preconditions.notBlank(basePackageName, "basePackageName must not be blank");
76+
Preconditions.notBlank(basePackageName, "basePackageName must not be null or blank");
7377

7478
List<File> dirs = allSourceDirsForPackage(basePackageName);
7579
return allClassesInSourceDirs(dirs, basePackageName, classFilter);
7680
}
7781

78-
private List<Class<?>> allClassesInSourceDirs(List<File> sourceDirs, String basePackageName,
79-
Predicate<Class<?>> classFilter) {
80-
List<Class<?>> classes = new ArrayList<>();
81-
for (File aSourceDir : sourceDirs) {
82-
classes.addAll(findClassesInSourceDirRecursively(aSourceDir, basePackageName, classFilter));
83-
}
84-
return classes;
85-
}
86-
8782
List<Class<?>> scanForClassesInClasspathRoot(File root, Predicate<Class<?>> classFilter) {
8883
Preconditions.notNull(root, "root must not be null");
8984
Preconditions.condition(root.isDirectory(),
9085
() -> "root must be an existing directory: " + root.getAbsolutePath());
9186

92-
return findClassesInSourceDirRecursively(root, "", classFilter);
87+
return findClassesInSourceDirRecursively(ROOT_PACKAGE_NAME, root, classFilter);
88+
}
89+
90+
private List<Class<?>> allClassesInSourceDirs(List<File> sourceDirs, String basePackageName,
91+
Predicate<Class<?>> classFilter) {
92+
93+
List<Class<?>> classes = new ArrayList<>();
94+
for (File sourceDir : sourceDirs) {
95+
classes.addAll(findClassesInSourceDirRecursively(basePackageName, sourceDir, classFilter));
96+
}
97+
return classes;
9398
}
9499

95100
private List<File> allSourceDirsForPackage(String basePackageName) {
96101
try {
97-
ClassLoader classLoader = classLoaderSupplier.get();
98-
String path = packagePath(basePackageName);
99-
Enumeration<URL> resources = classLoader.getResources(path);
102+
Enumeration<URL> resources = getClassLoader().getResources(packagePath(basePackageName));
100103
List<File> dirs = new ArrayList<>();
101104
while (resources.hasMoreElements()) {
102105
URL resource = resources.nextElement();
103106
dirs.add(new File(resource.getFile()));
104107
}
105108
return dirs;
106109
}
107-
catch (IOException e) {
110+
catch (Exception ex) {
108111
return Collections.emptyList();
109112
}
110113
}
111114

112-
private String packagePath(String basePackageName) {
113-
return basePackageName.replace('.', '/');
114-
}
115-
116-
private List<Class<?>> findClassesInSourceDirRecursively(File sourceDir, String packageName,
115+
private List<Class<?>> findClassesInSourceDirRecursively(String packageName, File sourceDir,
117116
Predicate<Class<?>> classFilter) {
117+
118118
Preconditions.notNull(classFilter, "classFilter must not be null");
119119

120-
List<Class<?>> classesCollector = new ArrayList<>();
121-
collectClassesRecursively(sourceDir, packageName, classesCollector, classFilter);
122-
return classesCollector;
120+
List<Class<?>> classes = new ArrayList<>();
121+
collectClassesRecursively(packageName, sourceDir, classFilter, classes);
122+
return classes;
123123
}
124124

125-
private void collectClassesRecursively(File sourceDir, String packageName, List<Class<?>> classesCollector,
126-
Predicate<Class<?>> classFilter) {
125+
private void collectClassesRecursively(String packageName, File sourceDir, Predicate<Class<?>> classFilter,
126+
List<Class<?>> classes) {
127+
127128
File[] files = sourceDir.listFiles();
128129
if (files == null) {
129130
return;
130131
}
132+
131133
for (File file : files) {
132134
if (isClassFile(file)) {
133-
this.handleClassFileSafely(packageName, classesCollector, classFilter, file);
135+
processClassFileSafely(packageName, file, classFilter, classes);
134136
}
135137
else if (file.isDirectory()) {
136-
collectClassesRecursively(file, appendPackageName(packageName, file.getName()), classesCollector,
137-
classFilter);
138+
collectClassesRecursively(appendSubpackageName(packageName, file.getName()), file, classFilter,
139+
classes);
138140
}
139141
}
140142
}
141143

142-
private void handleClassFileSafely(String packageName, List<Class<?>> classesCollector,
143-
Predicate<Class<?>> classFilter, File file) {
144-
Optional<Class<?>> classForClassFile = Optional.empty();
144+
private void processClassFileSafely(String packageName, File classFile, Predicate<Class<?>> classFilter,
145+
List<Class<?>> classes) {
145146

147+
Optional<Class<?>> clazz = Optional.empty();
146148
try {
147-
classForClassFile = loadClassForClassFile(file, packageName);
148-
classForClassFile.filter(classFilter).ifPresent(classesCollector::add);
149+
clazz = loadClassFromFile(packageName, classFile);
150+
clazz.filter(classFilter).ifPresent(classes::add);
149151
}
150152
catch (InternalError internalError) {
151-
this.catchInternalError(file, classForClassFile, internalError);
153+
handleInternalError(classFile, clazz, internalError);
152154
}
153155
catch (Throwable throwable) {
154-
this.catchAllOtherThrowables(file, throwable);
156+
handleThrowable(classFile, throwable);
155157
}
156158
}
157159

158-
private void catchInternalError(File file, Optional<Class<?>> classForClassFile, InternalError internalError) {
159-
// Malformed class name InternalError as reported by #401
160-
if (internalError.getMessage().equals("Malformed class name")) {
161-
this.logMalformedClassnameInternalError(file, classForClassFile);
160+
private Optional<Class<?>> loadClassFromFile(String packageName, File classFile) {
161+
String className = packageName + '.'
162+
+ classFile.getName().substring(0, classFile.getName().length() - CLASS_FILE_SUFFIX.length());
163+
return this.loadClass.apply(className, getClassLoader());
164+
}
165+
166+
private void handleInternalError(File classFile, Optional<Class<?>> clazz, InternalError ex) {
167+
if (MALFORMED_CLASS_NAME_ERROR_MESSAGE.equals(ex.getMessage())) {
168+
logMalformedClassName(classFile, clazz, ex);
162169
}
163-
// other potential InternalErrors
164170
else {
165-
this.logGenericFileProcessingProblem(file);
171+
logGenericFileProcessingException(classFile, ex);
166172
}
167173
}
168174

169-
private void logMalformedClassnameInternalError(File file, Optional<Class<?>> classForClassFile) {
175+
private void handleThrowable(File classFile, Throwable throwable) {
176+
rethrowIfBlacklisted(throwable);
177+
logGenericFileProcessingException(classFile, throwable);
178+
}
179+
180+
private void logMalformedClassName(File classFile, Optional<Class<?>> clazz, InternalError ex) {
170181
try {
171-
LOG.warning(() -> format("The class in the current file has a malformed class name. Offending file: '%s'",
172-
file.getAbsolutePath()));
173182

174-
classForClassFile.ifPresent(malformedClass ->
175-
//cannot use getCanonicalName() here because its being null is related to the underlying error
176-
LOG.warning(() -> format("Malformed class name: '%s'", malformedClass.getName())));
183+
if (clazz.isPresent()) {
184+
// Do not use getSimpleName() or getCanonicalName() here because they will likely
185+
// throw another exception due to the underlying error.
186+
logWarning(ex,
187+
() -> format("The java.lang.Class loaded from file [%s] has a malformed class name [%s].",
188+
classFile.getAbsolutePath(), clazz.get().getName()));
189+
}
190+
else {
191+
logWarning(ex, () -> format("The java.lang.Class loaded from file [%s] has a malformed class name.",
192+
classFile.getAbsolutePath()));
193+
}
177194
}
178-
catch (Throwable throwable) {
179-
LOG.warning(
180-
"The class name of the class in the current file is so malformed that not even getName() or toString() can be called on it!");
195+
catch (Throwable t) {
196+
ex.addSuppressed(t);
197+
logGenericFileProcessingException(classFile, ex);
181198
}
182199
}
183200

184-
private void catchAllOtherThrowables(File file, Throwable throwable) {
185-
rethrowIfBlacklisted(throwable);
186-
this.logGenericFileProcessingProblem(file);
187-
}
188-
189-
private void logGenericFileProcessingProblem(File file) {
190-
LOG.warning(() -> format("There was a problem while processing the current file. Offending file: '%s'",
191-
file.getAbsolutePath()));
201+
private void logGenericFileProcessingException(File classFile, Throwable throwable) {
202+
logWarning(throwable, () -> format("Failed to load java.lang.Class for file [%s] during classpath scanning.",
203+
classFile.getAbsolutePath()));
192204
}
193205

194-
private String appendPackageName(String packageName, String subpackageName) {
195-
if (packageName.isEmpty())
196-
return subpackageName;
197-
else
198-
return packageName + "." + subpackageName;
206+
private String appendSubpackageName(String packageName, String subpackageName) {
207+
return (!packageName.isEmpty() ? packageName + "." + subpackageName : subpackageName);
199208
}
200209

201-
private Optional<Class<?>> loadClassForClassFile(File file, String packageName) {
202-
String className = packageName + '.'
203-
+ file.getName().substring(0, file.getName().length() - CLASS_FILE_SUFFIX.length());
204-
return loadClass.apply(className, classLoaderSupplier.get());
210+
private ClassLoader getClassLoader() {
211+
return this.classLoaderSupplier.get();
205212
}
206213

207214
private static boolean isClassFile(File file) {
208215
return file.isFile() && file.getName().endsWith(CLASS_FILE_SUFFIX);
209216
}
210217

218+
private static String packagePath(String packageName) {
219+
return packageName.replace('.', '/');
220+
}
221+
222+
private static void logWarning(Throwable throwable, Supplier<String> msgSupplier) {
223+
LOG.log(Level.WARNING, throwable, msgSupplier);
224+
}
225+
211226
}

0 commit comments

Comments
 (0)