Skip to content

Method: ClasspathScanner(Consumer)

1: /*
2: * JB4JSON-LD
3: * Copyright (C) 2024 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.jsonld.deserialization.util;
19:
20: import cz.cvut.kbss.jsonld.exception.JsonLdException;
21: import org.slf4j.Logger;
22: import org.slf4j.LoggerFactory;
23:
24: import java.io.File;
25: import java.io.IOException;
26: import java.net.MalformedURLException;
27: import java.net.URI;
28: import java.net.URISyntaxException;
29: import java.net.URL;
30: import java.util.Enumeration;
31: import java.util.Objects;
32: import java.util.function.Consumer;
33: import java.util.jar.JarEntry;
34: import java.util.jar.JarFile;
35:
36: /**
37: * Processes classpath accessible to the application and passes all discovered classes to the registered listener.
38: */
39: public class ClasspathScanner {
40:
41: private static final Logger LOG = LoggerFactory.getLogger(ClasspathScanner.class);
42:
43: private static final String JAR_FILE_SUFFIX = ".jar";
44: private static final String CLASS_FILE_SUFFIX = ".class";
45:
46: private final Consumer<Class<?>> listener;
47:
48: public ClasspathScanner(Consumer<Class<?>> listener) {
49: this.listener = Objects.requireNonNull(listener);
50: }
51:
52: /**
53: * Scans classpath accessible from the current thread's class loader.
54: * <p>
55: * All available classes are passed to the registered consumer.
56: * <p>
57: * The {@code scanPath} parameter means that only the specified package (and it subpackages) should be searched.
58: * This parameter is optional, but it is highly recommended to specify it, as it can speed up the process
59: * dramatically.
60: * <p>
61: * Inspired by <a
62: * href="https://github.com/ddopson/java-class-enumerator">https://github.com/ddopson/java-class-enumerator</a>
63: *
64: * @param scanPath Package narrowing down the scan space. Optional
65: */
66: public void processClasses(String scanPath) {
67: if (scanPath == null) {
68: scanPath = "";
69: }
70: final ClassLoader loader = Thread.currentThread().getContextClassLoader();
71: try {
72: Enumeration<URL> urls = loader.getResources(scanPath.replace('.', '/'));
73: while (urls.hasMoreElements()) {
74: final URL url = urls.nextElement();
75: if (isJar(url.toString())) {
76: processJarFile(url, scanPath);
77: } else {
78: processDirectory(new File(getUrlAsUri(url).getPath()), scanPath);
79: }
80: }
81: // Scan jar files on classpath
82: Enumeration<URL> resources = loader.getResources(".");
83: while (resources.hasMoreElements()) {
84: URL resourceURL = resources.nextElement();
85: if (isJar(resourceURL.toString())) {
86: processJarFile(resourceURL, scanPath);
87: }
88: }
89: } catch (IOException e) {
90: throw new JsonLdException("Unable to scan packages.", e);
91: }
92: }
93:
94: private static boolean isJar(String filePath) {
95: return filePath.startsWith("jar:") || filePath.endsWith(JAR_FILE_SUFFIX);
96: }
97:
98: private static URI getUrlAsUri(URL url) {
99: try {
100: // Transformation to URI handles encoding, e.g. of whitespaces in the path
101: return url.toURI();
102: } catch (URISyntaxException ex) {
103: throw new JsonLdException("Unable to scan resource " + url + ". It is not a valid URI.", ex);
104: }
105: }
106:
107: protected void processJarFile(URL jarResource, String packageName) {
108: final String relPath = packageName.replace('.', '/');
109: final String jarPath = jarResource.getPath().replaceFirst("[.]jar/?!.*", JAR_FILE_SUFFIX)
110: .replaceFirst("file:", "")
111: .replaceFirst("nested:", "");
112:
113: LOG.trace("Scanning jar file {} for classes.", jarPath);
114: try (final JarFile jarFile = new JarFile(jarPath)) {
115: final Enumeration<JarEntry> entries = jarFile.entries();
116: while (entries.hasMoreElements()) {
117: final JarEntry entry = entries.nextElement();
118: final String entryName = entry.getName();
119: String className = null;
120: if (shouldSkipEntry(entryName)) {
121: continue;
122: }
123: if (entryName.endsWith(CLASS_FILE_SUFFIX) && entryName.contains(relPath)) {
124: // Remove prefix from multi-release JAR class names
125: className = entryName.replaceFirst("META-INF/versions/[1-9][0-9]*/", "");
126: className = className.replaceFirst("WEB-INF/classes/", "");
127: className = className.replaceFirst("BOOT-INF/classes/", "");
128: className = className.replace('/', '.').replace('\\', '.');
129: className = className.substring(0, className.length() - CLASS_FILE_SUFFIX.length());
130: }
131: if (className != null) {
132: processClass(className);
133: }
134: }
135: } catch (IOException e) {
136: LOG.error("Unable to scan classes in JAR file " + jarPath, e);
137: }
138: }
139:
140: private static boolean shouldSkipEntry(String entryName) {
141: // Skip module-info.class files
142: return entryName.endsWith("module-info" + CLASS_FILE_SUFFIX);
143: }
144:
145: private void processClass(String className) {
146: try {
147: final Class<?> cls = Class.forName(className);
148: listener.accept(cls);
149: } catch (Throwable e) {
150: LOG.debug("Skipping non-loadable class {}, got error {}: {}.", className, e.getClass()
151: .getName(), e.getMessage());
152: }
153: }
154:
155: private void processDirectory(File dir, String packageName)
156: throws MalformedURLException {
157: LOG.trace("Scanning directory {}.", dir);
158: // Get the list of the files contained in the package
159: final String[] files = dir.list();
160: if (files == null) {
161: return;
162: }
163: for (String fileName : files) {
164: String className = null;
165: // we are only interested in .class files
166: if (fileName.endsWith(CLASS_FILE_SUFFIX)) {
167: // removes the .class extension
168: className = packageName + '.' + fileName.substring(0, fileName.length() - 6);
169: }
170: if (className != null) {
171: processClass(className);
172: }
173: final File subDir = new File(dir, fileName);
174: if (subDir.isDirectory()) {
175: processDirectory(subDir, packageName + (!packageName.isEmpty() ? '.' : "") + fileName);
176: } else if (isJar(subDir.getAbsolutePath())) {
177: processJarFile(subDir.toURI().toURL(), packageName);
178: }
179: }
180: }
181: }