Skip to content

Method: sanitizePath(URL)

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