• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

coditory / quark-context / #18

pending completion
#18

push

github-actions

pmendelski
Fix context scanner

191 of 191 new or added lines in 3 files covered. (100.0%)

920 of 1126 relevant lines covered (81.71%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.65
/src/main/java/com/coditory/quark/context/ClassPath.java
1
package com.coditory.quark.context;
2

3
import org.slf4j.Logger;
4
import org.slf4j.LoggerFactory;
5

6
import java.io.File;
7
import java.io.IOException;
8
import java.net.MalformedURLException;
9
import java.net.URISyntaxException;
10
import java.net.URL;
11
import java.net.URLClassLoader;
12
import java.util.ArrayList;
13
import java.util.Arrays;
14
import java.util.Enumeration;
15
import java.util.HashSet;
16
import java.util.LinkedHashMap;
17
import java.util.LinkedHashSet;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Set;
21
import java.util.jar.Attributes;
22
import java.util.jar.JarEntry;
23
import java.util.jar.JarFile;
24
import java.util.jar.Manifest;
25

26
import static java.util.Collections.unmodifiableList;
27
import static java.util.Collections.unmodifiableMap;
28
import static java.util.Collections.unmodifiableSet;
29
import static java.util.Objects.requireNonNull;
30
import static java.util.stream.Collectors.toSet;
31

32
final class ClassPath {
33
    private static final Logger logger = LoggerFactory.getLogger(ClassPath.class.getName());
1✔
34
    private static final String CLASS_FILE_NAME_EXTENSION = ".class";
35
    private static final String PATH_SEPARATOR_SYS_PROP = System.getProperty("path.separator");
1✔
36
    private static final String JAVA_CLASS_PATH_SYS_PROP = System.getProperty("java.class.path");
1✔
37

38
    private final Set<ResourceInfo> resources;
39

40
    private ClassPath(Set<ResourceInfo> resources) {
1✔
41
        this.resources = resources;
1✔
42
    }
1✔
43

44
    public static ClassPath from(ClassLoader classloader) throws IOException {
45
        requireNonNull(classloader);
1✔
46
        Set<LocationInfo> locations = locationsFrom(classloader);
1✔
47
        Set<File> scanned = new LinkedHashSet<>();
1✔
48
        for (LocationInfo location : locations) {
1✔
49
            scanned.add(location.file());
1✔
50
        }
1✔
51
        Set<ResourceInfo> resources = new LinkedHashSet<>();
1✔
52
        for (LocationInfo location : locations) {
1✔
53
            resources.addAll(location.scanResources(scanned));
1✔
54
        }
1✔
55
        return new ClassPath(resources);
1✔
56
    }
57

58
    public Set<ClassInfo> getTopLevelClasses() {
59
        return resources.stream()
1✔
60
                .filter(r -> r instanceof ClassInfo)
1✔
61
                .map(r -> (ClassInfo) r)
1✔
62
                .filter(ClassInfo::isTopLevel)
1✔
63
                .collect(toSet());
1✔
64
    }
65

66
    public Set<ClassInfo> getTopLevelClassesRecursive(String packageName) {
67
        requireNonNull(packageName);
1✔
68
        String packagePrefix = packageName + '.';
1✔
69
        Set<ClassInfo> classes = new LinkedHashSet<>();
1✔
70
        for (ClassInfo classInfo : getTopLevelClasses()) {
1✔
71
            if (classInfo.getName().startsWith(packagePrefix)) {
1✔
72
                classes.add(classInfo);
1✔
73
            }
74
        }
1✔
75
        return unmodifiableSet(classes);
1✔
76
    }
77

78
    public static class ResourceInfo {
79
        private final File file;
80
        private final String resourceName;
81

82
        final ClassLoader loader;
83

84
        static ResourceInfo of(File file, String resourceName, ClassLoader loader) {
85
            return resourceName.endsWith(CLASS_FILE_NAME_EXTENSION)
1✔
86
                    ? new ClassInfo(file, resourceName, loader)
1✔
87
                    : new ResourceInfo(file, resourceName, loader);
1✔
88
        }
89

90
        ResourceInfo(File file, String resourceName, ClassLoader loader) {
1✔
91
            this.file = requireNonNull(file);
1✔
92
            this.resourceName = requireNonNull(resourceName);
1✔
93
            this.loader = requireNonNull(loader);
1✔
94
        }
1✔
95

96
        public File getFile() {
97
            return file;
×
98
        }
99

100
        @Override
101
        public int hashCode() {
102
            return resourceName.hashCode();
1✔
103
        }
104

105
        @Override
106
        public boolean equals(Object obj) {
107
            if (obj instanceof ResourceInfo that) {
1✔
108
                return resourceName.equals(that.resourceName)
1✔
109
                        && loader == that.loader;
110
            }
111
            return false;
×
112
        }
113

114
        @Override
115
        public String toString() {
116
            return resourceName;
×
117
        }
118
    }
119

120
    public static final class ClassInfo extends ResourceInfo {
121
        private final String className;
122

123
        ClassInfo(File file, String resourceName, ClassLoader loader) {
124
            super(file, resourceName, loader);
1✔
125
            this.className = getClassName(resourceName);
1✔
126
        }
1✔
127

128
        public String getName() {
129
            return className;
1✔
130
        }
131

132
        public boolean isTopLevel() {
133
            return className.indexOf('$') == -1;
1✔
134
        }
135

136
        @Override
137
        public String toString() {
138
            return className;
×
139
        }
140
    }
141

142
    static Set<LocationInfo> locationsFrom(ClassLoader classloader) {
143
        Set<LocationInfo> locations = new LinkedHashSet<>();
1✔
144
        for (Map.Entry<File, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) {
1✔
145
            locations.add(new LocationInfo(entry.getKey(), entry.getValue()));
1✔
146
        }
1✔
147
        return unmodifiableSet(locations);
1✔
148
    }
149

150
    static final class LocationInfo {
151
        final File home;
152
        private final ClassLoader classloader;
153

154
        LocationInfo(File home, ClassLoader classloader) {
1✔
155
            this.home = requireNonNull(home);
1✔
156
            this.classloader = requireNonNull(classloader);
1✔
157
        }
1✔
158

159
        public File file() {
160
            return home;
1✔
161
        }
162

163
        public Set<ResourceInfo> scanResources(Set<File> scannedFiles) throws IOException {
164
            Set<ResourceInfo> resources = new LinkedHashSet<>();
1✔
165
            scannedFiles.add(home);
1✔
166
            scan(home, scannedFiles, resources);
1✔
167
            return unmodifiableSet(resources);
1✔
168
        }
169

170
        private void scan(File file, Set<File> scannedUris, Set<ResourceInfo> result)
171
                throws IOException {
172
            try {
173
                if (!file.exists()) {
1✔
174
                    return;
×
175
                }
176
            } catch (SecurityException e) {
×
177
                logger.warn("Cannot access " + file + ": " + e);
×
178
                return;
×
179
            }
1✔
180
            if (file.isDirectory()) {
1✔
181
                scanDirectory(file, result);
1✔
182
            } else {
183
                scanJar(file, scannedUris, result);
1✔
184
            }
185
        }
1✔
186

187
        private void scanJar(File file, Set<File> scannedUris, Set<ResourceInfo> result) throws IOException {
188
            JarFile jarFile;
189
            try {
190
                jarFile = new JarFile(file);
1✔
191
            } catch (IOException e) {
×
192
                // Not a jar file
193
                return;
×
194
            }
1✔
195
            try {
196
                for (File path : getClassPathFromManifest(file, jarFile.getManifest())) {
1✔
197
                    // We only scan each file once independent of the classloader that file might be
198
                    // associated with.
199
                    if (scannedUris.add(path.getCanonicalFile())) {
×
200
                        scan(path, scannedUris, result);
×
201
                    }
202
                }
×
203
                scanJarFile(jarFile, result);
1✔
204
            } finally {
205
                try {
206
                    jarFile.close();
1✔
207
                } catch (IOException ignored) { // similar to try-with-resources, but don't fail scanning
×
208
                }
1✔
209
            }
210
        }
1✔
211

212
        private void scanJarFile(JarFile file, Set<ResourceInfo> result) {
213
            Enumeration<JarEntry> entries = file.entries();
1✔
214
            while (entries.hasMoreElements()) {
1✔
215
                JarEntry entry = entries.nextElement();
1✔
216
                if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) {
1✔
217
                    continue;
1✔
218
                }
219
                result.add(ResourceInfo.of(new File(file.getName()), entry.getName(), classloader));
1✔
220
            }
1✔
221
        }
1✔
222

223
        private void scanDirectory(File directory, Set<ResourceInfo> result)
224
                throws IOException {
225
            Set<File> currentPath = new HashSet<>();
1✔
226
            currentPath.add(directory.getCanonicalFile());
1✔
227
            scanDirectory(directory, "", currentPath, result);
1✔
228
        }
1✔
229

230
        private void scanDirectory(
231
                File directory,
232
                String packagePrefix,
233
                Set<File> currentPath,
234
                Set<ResourceInfo> builder
235
        ) throws IOException {
236
            File[] files = directory.listFiles();
1✔
237
            if (files == null) {
1✔
238
                logger.warn("Cannot read directory " + directory);
×
239
                // IO error, just skip the directory
240
                return;
×
241
            }
242
            for (File f : files) {
1✔
243
                String name = f.getName();
1✔
244
                if (f.isDirectory()) {
1✔
245
                    File deref = f.getCanonicalFile();
1✔
246
                    if (currentPath.add(deref)) {
1✔
247
                        scanDirectory(deref, packagePrefix + name + "/", currentPath, builder);
1✔
248
                        currentPath.remove(deref);
1✔
249
                    }
250
                } else {
1✔
251
                    String resourceName = packagePrefix + name;
1✔
252
                    if (!resourceName.equals(JarFile.MANIFEST_NAME)) {
1✔
253
                        builder.add(ResourceInfo.of(f, resourceName, classloader));
1✔
254
                    }
255
                }
256
            }
257
        }
1✔
258

259
        @Override
260
        public boolean equals(Object obj) {
261
            if (obj instanceof LocationInfo that) {
×
262
                return home.equals(that.home) && classloader.equals(that.classloader);
×
263
            }
264
            return false;
×
265
        }
266

267
        @Override
268
        public int hashCode() {
269
            return home.hashCode();
1✔
270
        }
271

272
        @Override
273
        public String toString() {
274
            return home.toString();
×
275
        }
276
    }
277

278
    static Set<File> getClassPathFromManifest(File jarFile, Manifest manifest) {
279
        if (manifest == null) {
1✔
280
            return Set.of();
1✔
281
        }
282
        Set<File> result = new LinkedHashSet<>();
1✔
283
        String classpathAttribute = manifest
1✔
284
                .getMainAttributes()
1✔
285
                .getValue(Attributes.Name.CLASS_PATH.toString());
1✔
286
        if (classpathAttribute != null) {
1✔
287
            for (String path : classpathAttribute.split(" ")) {
×
288
                if (path.isBlank()) {
×
289
                    continue;
×
290
                }
291
                URL url;
292
                try {
293
                    url = getClassPathEntry(jarFile, path);
×
294
                } catch (MalformedURLException e) {
×
295
                    // Ignore bad entry
296
                    logger.warn("Invalid Class-Path entry: " + path);
×
297
                    continue;
×
298
                }
×
299
                if (url.getProtocol().equals("file")) {
×
300
                    result.add(toFile(url));
×
301
                }
302
            }
303
        }
304
        return unmodifiableSet(result);
1✔
305
    }
306

307
    static Map<File, ClassLoader> getClassPathEntries(ClassLoader classloader) {
308
        LinkedHashMap<File, ClassLoader> entries = new LinkedHashMap<>();
1✔
309
        // Search parent first, since it's the order ClassLoader#loadClass() uses.
310
        ClassLoader parent = classloader.getParent();
1✔
311
        if (parent != null) {
1✔
312
            entries.putAll(getClassPathEntries(parent));
1✔
313
        }
314
        for (URL url : getClassLoaderUrls(classloader)) {
1✔
315
            if (url.getProtocol().equals("file")) {
1✔
316
                File file = toFile(url);
1✔
317
                if (!entries.containsKey(file)) {
1✔
318
                    entries.put(file, classloader);
1✔
319
                }
320
            }
321
        }
1✔
322
        return unmodifiableMap(entries);
1✔
323
    }
324

325
    private static List<URL> getClassLoaderUrls(ClassLoader classloader) {
326
        if (classloader instanceof URLClassLoader) {
1✔
327
            return Arrays.asList(((URLClassLoader) classloader).getURLs());
×
328
        }
329
        if (classloader.equals(ClassLoader.getSystemClassLoader())) {
1✔
330
            return parseJavaClassPath();
1✔
331
        }
332
        return List.of();
1✔
333
    }
334

335
    private static List<URL> parseJavaClassPath() {
336
        List<URL> urls = new ArrayList<>();
1✔
337
        for (String entry : JAVA_CLASS_PATH_SYS_PROP.split(PATH_SEPARATOR_SYS_PROP)) {
1✔
338
            try {
339
                try {
340
                    urls.add(new File(entry).toURI().toURL());
1✔
341
                } catch (SecurityException e) { // File.toURI checks to see if the file is a directory
×
342
                    urls.add(new URL("file", null, new File(entry).getAbsolutePath()));
×
343
                }
1✔
344
            } catch (MalformedURLException e) {
×
345
                logger.warn("Malformed classpath entry: " + entry, e);
×
346
            }
1✔
347
        }
348
        return unmodifiableList(urls);
1✔
349
    }
350

351
    private static URL getClassPathEntry(File jarFile, String path) throws MalformedURLException {
352
        return new URL(jarFile.toURI().toURL(), path);
×
353
    }
354

355
    private static String getClassName(String filename) {
356
        int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length();
1✔
357
        return filename.substring(0, classNameEnd).replace('/', '.');
1✔
358
    }
359

360
    private static File toFile(URL url) {
361
        try {
362
            return new File(url.toURI()); // Accepts escaped characters like %20.
1✔
363
        } catch (URISyntaxException e) { // URL.toURI() doesn't escape chars.
×
364
            return new File(url.getPath()); // Accepts non-escaped chars like space.
×
365
        }
366
    }
367
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc