/
DefaultClasspathScanner.java
206 lines (188 loc) · 8.41 KB
/
DefaultClasspathScanner.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/*
* JOPA
* Copyright (C) 2023 Czech Technical University in Prague
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package cz.cvut.kbss.jopa.loaders;
import cz.cvut.kbss.jopa.exceptions.OWLPersistenceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Processes classes available to the current classloader.
*/
public class DefaultClasspathScanner implements ClasspathScanner {
private static final Logger LOG = LoggerFactory.getLogger(DefaultClasspathScanner.class);
protected static final char JAVA_CLASSPATH_SEPARATOR = '/';
protected static final char WINDOWS_FILE_SEPARATOR = '\\';
protected static final char JAVA_PACKAGE_SEPARATOR = '.';
protected static final String JAR_FILE_SUFFIX = ".jar";
protected static final String CLASS_FILE_SUFFIX = ".class";
protected final List<Consumer<Class<?>>> listeners = new ArrayList<>();
protected final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
protected String pathPattern;
protected Set<URL> visited;
@Override
public void addListener(Consumer<Class<?>> listener) {
listeners.add(listener);
}
/**
* Inspired by https://github.com/ddopson/java-class-enumerator
*/
@Override
public void processClasses(String scanPackage) {
this.pathPattern = scanPackage.replace(JAVA_PACKAGE_SEPARATOR, JAVA_CLASSPATH_SEPARATOR);
this.visited = new HashSet<>();
try {
Enumeration<URL> urls = classLoader.getResources(pathPattern);
processElements(urls, scanPackage);
// Scan jar files on classpath
Enumeration<URL> resources = classLoader.getResources(".");
processElements(resources, scanPackage);
} catch (IOException e) {
throw new OWLPersistenceException("Unable to scan packages for entity classes.", e);
}
}
protected void processElements(Enumeration<URL> urls, String scanPath) throws IOException {
while (urls.hasMoreElements()) {
final URL url = urls.nextElement();
if (visited.contains(url)) {
continue;
}
visited.add(url);
LOG.trace("Processing classpath element {}", url);
if (isJar(url.toString())) {
processJarFile(createJarFile(url));
} else {
processDirectory(new File(getUrlAsUri(url).getPath()), scanPath);
}
}
}
/**
* Handles possible non-ascii character encoding in the specified URL.
*
* @param url Resource URL (presumably leading to a local file)
* @return Decoded argument
* @throws UnsupportedEncodingException Should not happen, using standard UTF-8 encoding
*/
protected static String sanitizePath(URL url) throws UnsupportedEncodingException {
return URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8);
}
protected static boolean isJar(String filePath) {
return filePath.startsWith("jar:") || filePath.endsWith(JAR_FILE_SUFFIX);
}
protected static JarFile createJarFile(URL elementUrl) throws IOException {
final String jarPath = sanitizePath(elementUrl).replaceFirst("[.]jar/?!.*", JAR_FILE_SUFFIX)
.replaceFirst("file:", "")
.replaceFirst("nested:", "");
return new JarFile(jarPath);
}
protected static URI getUrlAsUri(URL url) {
try {
// Transformation to URI handles encoding, e.g. of whitespaces in the path
return url.toURI();
} catch (URISyntaxException ex) {
throw new OWLPersistenceException(
"Unable to scan resource " + url + ". It is not a valid URI.", ex);
}
}
/**
* Processes the specified {@link JarFile}, looking for classes in the configured package.
*
* @param jarFile JAR file to scan
*/
protected void processJarFile(final JarFile jarFile) {
LOG.trace("Scanning jar file {} for entity classes.", jarFile.getName());
try (final JarFile localFile = jarFile) {
final Enumeration<JarEntry> entries = localFile.entries();
while (entries.hasMoreElements()) {
final JarEntry entry = entries.nextElement();
final String entryName = entry.getName();
if (entryName.endsWith(CLASS_FILE_SUFFIX) && entryName.contains(pathPattern)) {
String className = entryName.substring(entryName.indexOf(pathPattern));
className = className.replace(JAVA_CLASSPATH_SEPARATOR, JAVA_PACKAGE_SEPARATOR)
.replace(WINDOWS_FILE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
className = className.substring(0, className.length() - CLASS_FILE_SUFFIX.length());
processClass(className);
}
}
} catch (IOException e) {
throw new OWLPersistenceException("Unexpected IOException reading JAR File " + jarFile, e);
}
}
/**
* Retrieves a {@link Class} with the specified name and passes it to the registered listeners.
*
* @param className Fully-qualified class name
*/
protected void processClass(String className) {
try {
final Class<?> cls = Class.forName(className, true, classLoader);
listeners.forEach(listener -> listener.accept(cls));
} catch (Exception | NoClassDefFoundError e) {
LOG.debug("Unable to load class {}, got error {}: {}. Skipping the class. If it is an entity class, ensure it is available on classpath and is built with supported Java version.", className, e.getClass()
.getName(), e.getMessage());
}
}
/**
* Processes the specified directory, looking for classes in the specified package (and its descendants).
*
* @param dir Directory
* @param packageName Package name
*/
protected void processDirectory(File dir, String packageName) throws IOException {
if (!dir.getPath().replace(WINDOWS_FILE_SEPARATOR, JAVA_CLASSPATH_SEPARATOR).contains(pathPattern)) {
return;
}
LOG.trace("Scanning directory {} for entity classes.", dir);
// Get the list of the files contained in the package
final String[] files = dir.list();
if (files == null) {
return;
}
for (String fileName : files) {
String className = null;
// we are only interested in .class files
if (fileName.endsWith(CLASS_FILE_SUFFIX)) {
// removes the .class extension
className = packageName + '.' + fileName.substring(0, fileName.length() - 6);
}
if (className != null) {
processClass(className);
}
final File subDir = new File(dir, fileName);
if (subDir.isDirectory()) {
processDirectory(subDir, packageName + (!packageName.isEmpty() ? JAVA_PACKAGE_SEPARATOR : "") + fileName);
} else if (isJar(subDir.getAbsolutePath())) {
processJarFile(createJarFile(subDir.toURI().toURL()));
}
}
}
}