Skip to content

Method: processClass(String)

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