Skip to content

Package: DefaultClasspathScanner

DefaultClasspathScanner

nameinstructionbranchcomplexitylinemethod
DefaultClasspathScanner()
M: 0 C: 12
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 3
100%
M: 0 C: 1
100%
addListener(Consumer)
M: 0 C: 6
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 2
100%
M: 0 C: 1
100%
createJarFile(URL)
M: 0 C: 17
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 4
100%
M: 0 C: 1
100%
getUrlAsUri(URL)
M: 8 C: 3
27%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 2 C: 1
33%
M: 0 C: 1
100%
isJar(String)
M: 0 C: 12
100%
M: 0 C: 4
100%
M: 0 C: 3
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
lambda$processClass$0(Class, Consumer)
M: 0 C: 4
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
processClass(String)
M: 0 C: 34
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 7
100%
M: 0 C: 1
100%
processClasses(String)
M: 7 C: 32
82%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 2 C: 8
80%
M: 0 C: 1
100%
processDirectory(File, String)
M: 1 C: 90
99%
M: 1 C: 15
94%
M: 1 C: 8
89%
M: 1 C: 17
94%
M: 0 C: 1
100%
processElements(Enumeration, String)
M: 0 C: 42
100%
M: 0 C: 6
100%
M: 0 C: 4
100%
M: 0 C: 11
100%
M: 0 C: 1
100%
processJarFile(JarFile)
M: 8 C: 59
88%
M: 1 C: 5
83%
M: 1 C: 3
75%
M: 2 C: 15
88%
M: 0 C: 1
100%
sanitizePath(URL)
M: 0 C: 5
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
static {...}
M: 0 C: 4
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 1
100%
M: 0 C: 1
100%

Coverage

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);
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: .replaceFirst("nested:", "");
119: return new JarFile(jarPath);
120: }
121:
122: protected static URI getUrlAsUri(URL url) {
123: try {
124: // Transformation to URI handles encoding, e.g. of whitespaces in the path
125: return url.toURI();
126: } catch (URISyntaxException ex) {
127: throw new OWLPersistenceException(
128: "Unable to scan resource " + url + ". It is not a valid URI.", ex);
129: }
130: }
131:
132: /**
133: * Processes the specified {@link JarFile}, looking for classes in the configured package.
134: *
135: * @param jarFile JAR file to scan
136: */
137: protected void processJarFile(final JarFile jarFile) {
138: LOG.trace("Scanning jar file {} for entity classes.", jarFile.getName());
139: try (final JarFile localFile = jarFile) {
140: final Enumeration<JarEntry> entries = localFile.entries();
141:• while (entries.hasMoreElements()) {
142: final JarEntry entry = entries.nextElement();
143: final String entryName = entry.getName();
144:• if (entryName.endsWith(CLASS_FILE_SUFFIX) && entryName.contains(pathPattern)) {
145: String className = entryName.substring(entryName.indexOf(pathPattern));
146: className = className.replace(JAVA_CLASSPATH_SEPARATOR, JAVA_PACKAGE_SEPARATOR)
147: .replace(WINDOWS_FILE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
148: className = className.substring(0, className.length() - CLASS_FILE_SUFFIX.length());
149: processClass(className);
150: }
151: }
152: } catch (IOException e) {
153: throw new OWLPersistenceException("Unexpected IOException reading JAR File " + jarFile, e);
154: }
155: }
156:
157: /**
158: * Retrieves a {@link Class} with the specified name and passes it to the registered listeners.
159: *
160: * @param className Fully-qualified class name
161: */
162: protected void processClass(String className) {
163: try {
164: final Class<?> cls = Class.forName(className, true, classLoader);
165: listeners.forEach(listener -> listener.accept(cls));
166: } catch (Exception | NoClassDefFoundError e) {
167: 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()
168: .getName(), e.getMessage());
169: }
170: }
171:
172: /**
173: * Processes the specified directory, looking for classes in the specified package (and its descendants).
174: *
175: * @param dir Directory
176: * @param packageName Package name
177: */
178: protected void processDirectory(File dir, String packageName) throws IOException {
179:• if (!dir.getPath().replace(WINDOWS_FILE_SEPARATOR, JAVA_CLASSPATH_SEPARATOR).contains(pathPattern)) {
180: return;
181: }
182: LOG.trace("Scanning directory {} for entity classes.", dir);
183: // Get the list of the files contained in the package
184: final String[] files = dir.list();
185:• if (files == null) {
186: return;
187: }
188:• for (String fileName : files) {
189: String className = null;
190: // we are only interested in .class files
191:• if (fileName.endsWith(CLASS_FILE_SUFFIX)) {
192: // removes the .class extension
193: className = packageName + '.' + fileName.substring(0, fileName.length() - 6);
194: }
195:• if (className != null) {
196: processClass(className);
197: }
198: final File subDir = new File(dir, fileName);
199:• if (subDir.isDirectory()) {
200:• processDirectory(subDir, packageName + (!packageName.isEmpty() ? JAVA_PACKAGE_SEPARATOR : "") + fileName);
201:• } else if (isJar(subDir.getAbsolutePath())) {
202: processJarFile(createJarFile(subDir.toURI().toURL()));
203: }
204: }
205: }
206: }