1 /*
2 * This file is part of ClassGraph.
3 *
4 * Author: Luke Hutchison
5 *
6 * Hosted at: https://github.com/classgraph/classgraph
7 *
8 * --
9 *
10 * The MIT License (MIT)
11 *
12 * Copyright (c) 2019 Luke Hutchison
13 *
14 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
15 * documentation files (the "Software"), to deal in the Software without restriction, including without
16 * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
17 * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
18 * conditions:
19 *
20 * The above copyright notice and this permission notice shall be included in all copies or substantial
21 * portions of the Software.
22 *
23 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
24 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
25 * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
26 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
27 * OR OTHER DEALINGS IN THE SOFTWARE.
28 */
29 package io.github.classgraph;
30
31 import java.io.File;
32 import java.io.FileNotFoundException;
33 import java.io.IOException;
34 import java.net.MalformedURLException;
35 import java.net.URL;
36 import java.net.URLDecoder;
37 import java.util.AbstractMap.SimpleEntry;
38 import java.util.ArrayList;
39 import java.util.Collection;
40 import java.util.Collections;
41 import java.util.Comparator;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Map.Entry;
47 import java.util.Queue;
48 import java.util.Set;
49 import java.util.concurrent.Callable;
50 import java.util.concurrent.CancellationException;
51 import java.util.concurrent.ConcurrentHashMap;
52 import java.util.concurrent.ConcurrentLinkedQueue;
53 import java.util.concurrent.ExecutionException;
54 import java.util.concurrent.ExecutorService;
55
56 import io.github.classgraph.ClassGraph.FailureHandler;
57 import io.github.classgraph.ClassGraph.ScanResultProcessor;
58 import io.github.classgraph.Classfile.ClassfileFormatException;
59 import io.github.classgraph.Classfile.SkipClassException;
60 import nonapi.io.github.classgraph.classpath.ClasspathFinder;
61 import nonapi.io.github.classgraph.classpath.ClasspathOrder.ClasspathElementAndClassLoader;
62 import nonapi.io.github.classgraph.classpath.ModuleFinder;
63 import nonapi.io.github.classgraph.concurrency.AutoCloseableExecutorService;
64 import nonapi.io.github.classgraph.concurrency.InterruptionChecker;
65 import nonapi.io.github.classgraph.concurrency.SingletonMap;
66 import nonapi.io.github.classgraph.concurrency.SingletonMap.NullSingletonException;
67 import nonapi.io.github.classgraph.concurrency.WorkQueue;
68 import nonapi.io.github.classgraph.concurrency.WorkQueue.WorkUnitProcessor;
69 import nonapi.io.github.classgraph.fastzipfilereader.NestedJarHandler;
70 import nonapi.io.github.classgraph.scanspec.ScanSpec;
71 import nonapi.io.github.classgraph.utils.CollectionUtils;
72 import nonapi.io.github.classgraph.utils.FastPathResolver;
73 import nonapi.io.github.classgraph.utils.FileUtils;
74 import nonapi.io.github.classgraph.utils.JarUtils;
75 import nonapi.io.github.classgraph.utils.LogNode;
76
77 /** The classpath scanner. */
78 class Scanner implements Callable<ScanResult> {
79
80 /** The scan spec. */
81 private final ScanSpec scanSpec;
82
83 /** If true, performing a scan. If false, only fetching the classpath. */
84 public boolean performScan;
85
86 /** The nested jar handler. */
87 private final NestedJarHandler nestedJarHandler;
88
89 /** The executor service. */
90 private final ExecutorService executorService;
91
92 /** The interruption checker. */
93 private final InterruptionChecker interruptionChecker;
94
95 /** The number of parallel tasks. */
96 private final int numParallelTasks;
97
98 /** The scan result processor. */
99 private final ScanResultProcessor scanResultProcessor;
100
101 /** The failure handler. */
102 private final FailureHandler failureHandler;
103
104 /** The toplevel log. */
105 private final LogNode topLevelLog;
106
107 /** The classpath finder. */
108 private final ClasspathFinder classpathFinder;
109
110 /** The module order. */
111 private final List<ClasspathElementModule> moduleOrder;
112
113 /** The environment classloader order, respecting parent-first or parent-last delegation order. */
114 private final ClassLoader[] classLoaderOrderRespectingParentDelegation;
115
116 // -------------------------------------------------------------------------------------------------------------
117
118 /**
119 * The classpath scanner. Scanning is started by calling {@link #call()} on this object.
120 *
121 * @param performScan
122 * If true, performing a scan. If false, only fetching the classpath.
123 * @param scanSpec
124 * the scan spec
125 * @param executorService
126 * the executor service
127 * @param numParallelTasks
128 * the num parallel tasks
129 * @param scanResultProcessor
130 * the scan result processor
131 * @param failureHandler
132 * the failure handler
133 * @param topLevelLog
134 * the log
135 *
136 * @throws InterruptedException
137 * if interrupted
138 */
139 Scanner(final boolean performScan, final ScanSpec scanSpec, final ExecutorService executorService,
140 final int numParallelTasks, final ScanResultProcessor scanResultProcessor,
141 final FailureHandler failureHandler, final LogNode topLevelLog) throws InterruptedException {
142 this.scanSpec = scanSpec;
143 this.performScan = performScan;
144 scanSpec.sortPrefixes();
145 scanSpec.log(topLevelLog);
146 if (topLevelLog != null) {
147 if (scanSpec.pathWhiteBlackList != null
148 && scanSpec.packagePrefixWhiteBlackList.isSpecificallyWhitelisted("")) {
149 topLevelLog.log("Note: There is no need to whitelist the root package (\"\") -- not whitelisting "
150 + "anything will have the same effect of causing all packages to be scanned");
151 }
152 topLevelLog.log("Number of worker threads: " + numParallelTasks);
153 }
154
155 this.executorService = executorService;
156 this.interruptionChecker = executorService instanceof AutoCloseableExecutorService
157 ? ((AutoCloseableExecutorService) executorService).interruptionChecker
158 : new InterruptionChecker();
159 this.nestedJarHandler = new NestedJarHandler(scanSpec, interruptionChecker);
160 this.numParallelTasks = numParallelTasks;
161 this.scanResultProcessor = scanResultProcessor;
162 this.failureHandler = failureHandler;
163 this.topLevelLog = topLevelLog;
164
165 final LogNode classpathFinderLog = topLevelLog == null ? null : topLevelLog.log("Finding classpath");
166 this.classpathFinder = new ClasspathFinder(scanSpec, classpathFinderLog);
167 this.classLoaderOrderRespectingParentDelegation = classpathFinder
168 .getClassLoaderOrderRespectingParentDelegation();
169
170 try {
171 this.moduleOrder = new ArrayList<>();
172
173 // Check if modules should be scanned
174 final ModuleFinder moduleFinder = classpathFinder.getModuleFinder();
175 if (moduleFinder != null) {
176 // Add modules to start of classpath order, before traditional classpath
177 final List<ModuleRef> systemModuleRefs = moduleFinder.getSystemModuleRefs();
178 final ClassLoader defaultClassLoader = classLoaderOrderRespectingParentDelegation != null
179 && classLoaderOrderRespectingParentDelegation.length != 0
180 ? classLoaderOrderRespectingParentDelegation[0]
181 : null;
182 if (systemModuleRefs != null) {
183 for (final ModuleRef systemModuleRef : systemModuleRefs) {
184 final String moduleName = systemModuleRef.getName();
185 if (
186 // If scanning system packages and modules is enabled and white/blacklist is empty,
187 // then scan all system modules
188 (scanSpec.enableSystemJarsAndModules
189 && scanSpec.moduleWhiteBlackList.whitelistAndBlacklistAreEmpty())
190 // Otherwise only scan specifically whitelisted system modules
191 || scanSpec.moduleWhiteBlackList
192 .isSpecificallyWhitelistedAndNotBlacklisted(moduleName)) {
193 // Create a new ClasspathElementModule
194 final ClasspathElementModule classpathElementModule = new ClasspathElementModule(
195 systemModuleRef, defaultClassLoader,
196 nestedJarHandler.moduleRefToModuleReaderProxyRecyclerMap, scanSpec);
197 moduleOrder.add(classpathElementModule);
198 // Open the ClasspathElementModule
199 classpathElementModule.open(/* ignored */ null, classpathFinderLog);
200 } else {
201 if (classpathFinderLog != null) {
202 classpathFinderLog.log(
203 "Skipping non-whitelisted or blacklisted system module: " + moduleName);
204 }
205 }
206 }
207 }
208 final List<ModuleRef> nonSystemModuleRefs = moduleFinder.getNonSystemModuleRefs();
209 if (nonSystemModuleRefs != null) {
210 for (final ModuleRef nonSystemModuleRef : nonSystemModuleRefs) {
211 String moduleName = nonSystemModuleRef.getName();
212 if (moduleName == null) {
213 moduleName = "";
214 }
215 if (scanSpec.moduleWhiteBlackList.isWhitelistedAndNotBlacklisted(moduleName)) {
216 // Create a new ClasspathElementModule
217 final ClasspathElementModule classpathElementModule = new ClasspathElementModule(
218 nonSystemModuleRef, defaultClassLoader,
219 nestedJarHandler.moduleRefToModuleReaderProxyRecyclerMap, scanSpec);
220 moduleOrder.add(classpathElementModule);
221 // Open the ClasspathElementModule
222 classpathElementModule.open(/* ignored */ null, classpathFinderLog);
223 } else {
224 if (classpathFinderLog != null) {
225 classpathFinderLog
226 .log("Skipping non-whitelisted or blacklisted module: " + moduleName);
227 }
228 }
229 }
230 }
231 }
232 } catch (final InterruptedException e) {
233 nestedJarHandler.close(/* log = */ null);
234 throw e;
235 }
236 }
237
238 // -------------------------------------------------------------------------------------------------------------
239
240 /**
241 * Recursively perform a depth-first search of jar interdependencies, breaking cycles if necessary, to determine
242 * the final classpath element order.
243 *
244 * @param currClasspathElement
245 * the current classpath element
246 * @param visitedClasspathElts
247 * visited classpath elts
248 * @param order
249 * the classpath element order
250 */
251 private static void findClasspathOrderRec(final ClasspathElement currClasspathElement,
252 final Set<ClasspathElement> visitedClasspathElts, final List<ClasspathElement> order) {
253 if (visitedClasspathElts.add(currClasspathElement)) {
254 if (!currClasspathElement.skipClasspathElement) {
255 // Don't add a classpath element if it is marked to be skipped.
256 order.add(currClasspathElement);
257 }
258 // Whether or not a classpath element should be skipped, add any child classpath elements that are
259 // not marked to be skipped (i.e. keep recursing)
260 for (final ClasspathElement childClasspathElt : currClasspathElement.childClasspathElementsOrdered) {
261 findClasspathOrderRec(childClasspathElt, visitedClasspathElts, order);
262 }
263 }
264 }
265
266 /** Comparator used to sort ClasspathElement values into increasing order of integer index key. */
267 private static final Comparator<Entry<Integer, ClasspathElement>> INDEXED_CLASSPATH_ELEMENT_COMPARATOR = //
268 new Comparator<Map.Entry<Integer, ClasspathElement>>() {
269 @Override
270 public int compare(final Entry<Integer, ClasspathElement> o1,
271 final Entry<Integer, ClasspathElement> o2) {
272 return o1.getKey() - o2.getKey();
273 }
274 };
275
276 /**
277 * Sort a collection of indexed ClasspathElements into increasing order of integer index key.
278 *
279 * @param classpathEltsIndexed
280 * the indexed classpath elts
281 * @return the classpath elements, ordered by index
282 */
283 private static List<ClasspathElement> orderClasspathElements(
284 final Collection<Entry<Integer, ClasspathElement>> classpathEltsIndexed) {
285 final List<Entry<Integer, ClasspathElement>> classpathEltsIndexedOrdered = new ArrayList<>(
286 classpathEltsIndexed);
287 CollectionUtils.sortIfNotEmpty(classpathEltsIndexedOrdered, INDEXED_CLASSPATH_ELEMENT_COMPARATOR);
288 final List<ClasspathElement> classpathEltsOrdered = new ArrayList<>(classpathEltsIndexedOrdered.size());
289 for (final Entry<Integer, ClasspathElement> ent : classpathEltsIndexedOrdered) {
290 classpathEltsOrdered.add(ent.getValue());
291 }
292 return classpathEltsOrdered;
293 }
294
295 /**
296 * Recursively perform a depth-first traversal of child classpath elements, breaking cycles if necessary, to
297 * determine the final classpath element order. This causes child classpath elements to be inserted in-place in
298 * the classpath order, after the parent classpath element that contained them.
299 *
300 * @param uniqueClasspathElements
301 * the unique classpath elements
302 * @param toplevelClasspathEltsIndexed
303 * the toplevel classpath elts, indexed by order within the toplevel classpath
304 * @return the final classpath order, after depth-first traversal of child classpath elements
305 */
306 private List<ClasspathElement> findClasspathOrder(final Set<ClasspathElement> uniqueClasspathElements,
307 final Queue<Entry<Integer, ClasspathElement>> toplevelClasspathEltsIndexed) {
308 final List<ClasspathElement> toplevelClasspathEltsOrdered = orderClasspathElements(
309 toplevelClasspathEltsIndexed);
310 for (final ClasspathElement classpathElt : uniqueClasspathElements) {
311 classpathElt.childClasspathElementsOrdered = orderClasspathElements(
312 classpathElt.childClasspathElementsIndexed);
313 }
314 final Set<ClasspathElement> visitedClasspathElts = new HashSet<>();
315 final List<ClasspathElement> order = new ArrayList<>();
316 for (final ClasspathElement toplevelClasspathElt : toplevelClasspathEltsOrdered) {
317 findClasspathOrderRec(toplevelClasspathElt, visitedClasspathElts, order);
318 }
319 return order;
320 }
321
322 // -------------------------------------------------------------------------------------------------------------
323
324 /**
325 * Process work units.
326 *
327 * @param <W>
328 * the work unit type
329 * @param workUnits
330 * the work units
331 * @param log
332 * the log entry text to group work units under
333 * @param workUnitProcessor
334 * the work unit processor
335 * @throws InterruptedException
336 * if a worker was interrupted.
337 * @throws ExecutionException
338 * If a worker threw an uncaught exception.
339 */
340 private <W> void processWorkUnits(final Collection<W> workUnits, final LogNode log,
341 final WorkUnitProcessor<W> workUnitProcessor) throws InterruptedException, ExecutionException {
342 WorkQueue.runWorkQueue(workUnits, executorService, interruptionChecker, numParallelTasks, log,
343 workUnitProcessor);
344 if (log != null) {
345 log.addElapsedTime();
346 }
347 // Throw InterruptedException if any of the workers failed
348 interruptionChecker.check();
349 }
350
351 // -------------------------------------------------------------------------------------------------------------
352
353 /** Used to enqueue classpath elements for opening. */
354 static class ClasspathEntryWorkUnit {
355 /** The raw classpath entry and associated {@link ClassLoader}. */
356 private final ClasspathElementAndClassLoader rawClasspathEntry;
357
358 /** The parent classpath element. */
359 private final ClasspathElement parentClasspathElement;
360
361 /** The order within the parent classpath element. */
362 private final int orderWithinParentClasspathElement;
363
364 /**
365 * Constructor.
366 *
367 * @param rawClasspathEntry
368 * the raw classpath entry path and the classloader it was obtained from
369 * @param parentClasspathElement
370 * the parent classpath element
371 * @param orderWithinParentClasspathElement
372 * the order within parent classpath element
373 */
374 public ClasspathEntryWorkUnit(final ClasspathElementAndClassLoader rawClasspathEntry,
375 final ClasspathElement parentClasspathElement, final int orderWithinParentClasspathElement) {
376 this.rawClasspathEntry = rawClasspathEntry;
377 this.parentClasspathElement = parentClasspathElement;
378 this.orderWithinParentClasspathElement = orderWithinParentClasspathElement;
379 }
380 }
381
382 /**
383 * The classpath element singleton map. For each classpath element path, canonicalize path, and create a
384 * ClasspathElement singleton.
385 */
386 private final SingletonMap<ClasspathElementAndClassLoader, ClasspathElement, IOException> //
387 classpathEntryToClasspathElementSingletonMap = //
388 new SingletonMap<ClasspathElementAndClassLoader, ClasspathElement, IOException>() {
389 @Override
390 public ClasspathElement newInstance(final ClasspathElementAndClassLoader classpathEntry,
391 final LogNode log) throws IOException, InterruptedException {
392 final Object classpathEntryObj = classpathEntry.classpathElement;
393 String classpathEntryPath;
394 if (classpathEntryObj instanceof URL) {
395 URL classpathEntryURL = (URL) classpathEntryObj;
396 String scheme = classpathEntryURL.getProtocol();
397 if ("jar".equals(scheme)) {
398 // Strip off "jar:" scheme prefix
399 try {
400 classpathEntryURL = new URL(
401 URLDecoder.decode(classpathEntryURL.toString(), "UTF-8").substring(4));
402 scheme = classpathEntryURL.getProtocol();
403 } catch (final MalformedURLException e) {
404 throw new IOException("Could not strip 'jar:' prefix from " + classpathEntryObj, e);
405 }
406 }
407 if ("file".equals(scheme)) {
408 // Extract file path, and use below as a path string to determine if this
409 // classpath element is a file (jar) or directory
410 classpathEntryPath = URLDecoder.decode(classpathEntryURL.getPath(), "UTF-8");
411 } else if ("http".equals(scheme) || "https".equals(scheme)) {
412 // Jar URL or URI (remote URLs/URIs must be jars)
413 return new ClasspathElementZip(classpathEntryURL, classpathEntry.classLoader,
414 nestedJarHandler, scanSpec);
415 } else {
416 // For custom URL schemes, assume it must be for a jar, not a directory
417 return new ClasspathElementZip(classpathEntryURL, classpathEntry.classLoader,
418 nestedJarHandler, scanSpec);
419 }
420 } else {
421 // classpathEntryObj is a string
422 classpathEntryPath = classpathEntryObj.toString();
423 }
424
425 // Normalize path -- strip off any leading "jar:" / "file:", and normalize separators
426 final String pathNormalized = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH,
427 classpathEntryPath);
428
429 // "http:", "https:" or any other URL/URI scheme must indicate a jar
430 if (JarUtils.URL_SCHEME_PATTERN.matcher(pathNormalized).matches()) {
431 return new ClasspathElementZip(classpathEntryPath, classpathEntry.classLoader,
432 nestedJarHandler, scanSpec);
433 }
434
435 // Strip everything after first "!", to get path of base jarfile or dir
436 final int plingIdx = pathNormalized.indexOf('!');
437 final String pathToCanonicalize = plingIdx < 0 ? pathNormalized
438 : pathNormalized.substring(0, plingIdx);
439 // Canonicalize base jarfile or dir (may throw IOException)
440 final File fileCanonicalized = new File(pathToCanonicalize).getCanonicalFile();
441 // Test if base file or dir exists (and is a standard file or dir)
442 if (!fileCanonicalized.exists()) {
443 throw new FileNotFoundException();
444 }
445 if (!FileUtils.canRead(fileCanonicalized)) {
446 throw new IOException("Cannot read file or directory");
447 }
448 boolean isJar = classpathEntryPath.regionMatches(true, 0, "jar:", 0, 4) || plingIdx > 0;
449 if (fileCanonicalized.isFile()) {
450 // If a file, must be a jar
451 isJar = true;
452 } else if (fileCanonicalized.isDirectory()) {
453 if (isJar) {
454 throw new IOException("Expected jar, found directory");
455 }
456 } else {
457 throw new IOException("Not a normal file or directory");
458 }
459 // Check if canonicalized path is the same as pre-canonicalized path
460 final String baseFileCanonicalPathNormalized = FastPathResolver.resolve(FileUtils.CURR_DIR_PATH,
461 fileCanonicalized.getPath());
462 final String canonicalPathNormalized = plingIdx < 0 ? baseFileCanonicalPathNormalized
463 : baseFileCanonicalPathNormalized + pathNormalized.substring(plingIdx);
464 if (!canonicalPathNormalized.equals(pathNormalized)) {
465 // If canonicalized path is not the same as pre-canonicalized path, need to recurse
466 // to map non-canonicalized path to singleton for canonicalized path (this should
467 // only recurse once, since File::getCanonicalFile and FastPathResolver::resolve are
468 // idempotent)
469 try {
470 return this.get(new ClasspathElementAndClassLoader(canonicalPathNormalized,
471 classpathEntry.classLoader), log);
472 } catch (final NullSingletonException e) {
473 throw new IOException("Cannot get classpath element for canonical path "
474 + canonicalPathNormalized + " : " + e);
475 }
476 } else {
477 // Otherwise path is already canonical, and this is the first time this path has
478 // been seen -- instantiate a ClasspathElementZip or ClasspathElementDir singleton
479 // for the classpath element path
480 return isJar
481 ? new ClasspathElementZip(canonicalPathNormalized, classpathEntry.classLoader,
482 nestedJarHandler, scanSpec)
483 : new ClasspathElementDir(fileCanonicalized, classpathEntry.classLoader,
484 nestedJarHandler, scanSpec);
485 }
486 }
487 };
488
489 /**
490 * Create a WorkUnitProcessor for opening traditional classpath entries (which are mapped to
491 * {@link ClasspathElementDir} or {@link ClasspathElementZip} -- {@link ClasspathElementModule is handled
492 * separately}).
493 *
494 * @param openedClasspathElementsSet
495 * the opened classpath elements set
496 * @param toplevelClasspathEltOrder
497 * the toplevel classpath elt order
498 * @return the work unit processor
499 */
500 private WorkUnitProcessor<ClasspathEntryWorkUnit> newClasspathEntryWorkUnitProcessor(
501 final Set<ClasspathElement> openedClasspathElementsSet,
502 final Queue<Entry<Integer, ClasspathElement>> toplevelClasspathEltOrder) {
503 return new WorkUnitProcessor<ClasspathEntryWorkUnit>() {
504 @Override
505 public void processWorkUnit(final ClasspathEntryWorkUnit workUnit,
506 final WorkQueue<ClasspathEntryWorkUnit> workQueue, final LogNode log)
507 throws InterruptedException {
508 try {
509 // Create a ClasspathElementZip or ClasspathElementDir for each entry in the classpath
510 ClasspathElement classpathElt;
511 try {
512 classpathElt = classpathEntryToClasspathElementSingletonMap.get(workUnit.rawClasspathEntry,
513 log);
514 } catch (final NullSingletonException e) {
515 throw new IOException("Cannot get classpath element for classpath entry "
516 + workUnit.rawClasspathEntry + " : " + e);
517 }
518
519 // Only run open() once per ClasspathElement (it is possible for there to be
520 // multiple classpath elements with different non-canonical paths that map to
521 // the same canonical path, i.e. to the same ClasspathElement)
522 if (openedClasspathElementsSet.add(classpathElt)) {
523 // Check if the classpath element is valid (classpathElt.skipClasspathElement
524 // will be set if not). In case of ClasspathElementZip, open or extract nested
525 // jars as LogicalZipFile instances. Read manifest files for jarfiles to look
526 // for Class-Path manifest entries. Adds extra classpath elements to the work
527 // queue if they are found.
528 classpathElt.open(workQueue, log);
529
530 // Create a new tuple consisting of the order of the new classpath element
531 // within its parent, and the new classpath element.
532 // N.B. even if skipClasspathElement is true, still possibly need to scan child
533 // classpath elements (so still need to connect parent to child here)
534 final SimpleEntry<Integer, ClasspathElement> classpathEltEntry = //
535 new SimpleEntry<>(workUnit.orderWithinParentClasspathElement, classpathElt);
536 if (workUnit.parentClasspathElement != null) {
537 // Link classpath element to its parent, if it is not a toplevel element
538 workUnit.parentClasspathElement.childClasspathElementsIndexed.add(classpathEltEntry);
539 } else {
540 // Record toplevel elements
541 toplevelClasspathEltOrder.add(classpathEltEntry);
542 }
543 }
544 } catch (final IOException | SecurityException e) {
545 if (log != null) {
546 log.log("Skipping invalid classpath element " + workUnit.rawClasspathEntry.classpathElement
547 + " : " + e);
548 }
549 }
550 }
551 };
552 }
553
554 // -------------------------------------------------------------------------------------------------------------
555
556 /** Used to enqueue classfiles for scanning. */
557 static class ClassfileScanWorkUnit {
558
559 /** The classpath element. */
560 private final ClasspathElement classpathElement;
561
562 /** The classfile resource. */
563 private final Resource classfileResource;
564
565 /** True if this is an external class. */
566 private final boolean isExternalClass;
567
568 /**
569 * Constructor.
570 *
571 * @param classpathElement
572 * the classpath element
573 * @param classfileResource
574 * the classfile resource
575 * @param isExternalClass
576 * the is external class
577 */
578 ClassfileScanWorkUnit(final ClasspathElement classpathElement, final Resource classfileResource,
579 final boolean isExternalClass) {
580 this.classpathElement = classpathElement;
581 this.classfileResource = classfileResource;
582 this.isExternalClass = isExternalClass;
583 }
584 }
585
586 /** WorkUnitProcessor for scanning classfiles. */
587 private static class ClassfileScannerWorkUnitProcessor implements WorkUnitProcessor<ClassfileScanWorkUnit> {
588 /** The scan spec. */
589 private final ScanSpec scanSpec;
590
591 /** The classpath order. */
592 private final List<ClasspathElement> classpathOrder;
593
594 /**
595 * The names of whitelisted classes found in the classpath while scanning paths within classpath elements.
596 */
597 private final Set<String> whitelistedClassNamesFound;
598
599 /**
600 * The names of external (non-whitelisted) classes scheduled for extended scanning (where scanning is
601 * extended upwards to superclasses, interfaces and annotations).
602 */
603 private final Set<String> classNamesScheduledForExtendedScanning = Collections
604 .newSetFromMap(new ConcurrentHashMap<String, Boolean>());
605
606 /** The valid {@link Classfile} objects created by scanning classfiles. */
607 private final Queue<Classfile> scannedClassfiles;
608
609 /** The string intern map. */
610 private final ConcurrentHashMap<String, String> stringInternMap = new ConcurrentHashMap<>();
611
612 /**
613 * Constructor.
614 *
615 * @param scanSpec
616 * the scan spec
617 * @param classpathOrder
618 * the classpath order
619 * @param whitelistedClassNamesFound
620 * the names of whitelisted classes found in the classpath while scanning paths within classpath
621 * elements.
622 * @param scannedClassfiles
623 * the {@link Classfile} objects created by scanning classfiles
624 */
625 public ClassfileScannerWorkUnitProcessor(final ScanSpec scanSpec,
626 final List<ClasspathElement> classpathOrder, final Set<String> whitelistedClassNamesFound,
627 final Queue<Classfile> scannedClassfiles) {
628 this.scanSpec = scanSpec;
629 this.classpathOrder = classpathOrder;
630 this.whitelistedClassNamesFound = whitelistedClassNamesFound;
631 this.scannedClassfiles = scannedClassfiles;
632 }
633
634 /**
635 * Process work unit.
636 *
637 * @param workUnit
638 * the work unit
639 * @param workQueue
640 * the work queue
641 * @param log
642 * the log
643 * @throws InterruptedException
644 * the interrupted exception
645 */
646 /* (non-Javadoc)
647 * @see nonapi.io.github.classgraph.concurrency.WorkQueue.WorkUnitProcessor#processWorkUnit(
648 * java.lang.Object, nonapi.io.github.classgraph.concurrency.WorkQueue)
649 */
650 @Override
651 public void processWorkUnit(final ClassfileScanWorkUnit workUnit,
652 final WorkQueue<ClassfileScanWorkUnit> workQueue, final LogNode log) throws InterruptedException {
653 // Classfile scan log entries are listed inline below the entry that was added to the log
654 // when the path of the corresponding resource was found, by using the LogNode stored in
655 // Resource#scanLog. This allows the path scanning and classfile scanning logs to be
656 // merged into a single tree, rather than having them appear as two separate trees.
657 final LogNode subLog = workUnit.classfileResource.scanLog == null ? null
658 : workUnit.classfileResource.scanLog.log(workUnit.classfileResource.getPath(),
659 "Parsing classfile");
660 try {
661 // Parse classfile binary format, creating a Classfile object
662 final Classfile classfile = new Classfile(workUnit.classpathElement, classpathOrder,
663 whitelistedClassNamesFound, classNamesScheduledForExtendedScanning,
664 workUnit.classfileResource.getPath(), workUnit.classfileResource, workUnit.isExternalClass,
665 stringInternMap, workQueue, scanSpec, subLog);
666
667 // Enqueue the classfile for linking
668 scannedClassfiles.add(classfile);
669
670 } catch (final SkipClassException e) {
671 if (subLog != null) {
672 subLog.log(workUnit.classfileResource.getPath(), "Skipping classfile: " + e.getMessage());
673 }
674 } catch (final ClassfileFormatException e) {
675 if (subLog != null) {
676 subLog.log(workUnit.classfileResource.getPath(), "Invalid classfile: " + e.getMessage());
677 }
678 } catch (final IOException e) {
679 if (subLog != null) {
680 subLog.log(workUnit.classfileResource.getPath(), "Could not read classfile: " + e);
681 }
682 } finally {
683 if (subLog != null) {
684 subLog.addElapsedTime();
685 }
686 }
687 }
688 }
689
690 // -------------------------------------------------------------------------------------------------------------
691
692 /**
693 * Find classpath elements whose path is a prefix of another classpath element, and record the nesting.
694 *
695 * @param classpathElts
696 * the classpath elements
697 * @param log
698 * the log
699 */
700 private void findNestedClasspathElements(final List<SimpleEntry<String, ClasspathElement>> classpathElts,
701 final LogNode log) {
702 // Sort classpath elements into lexicographic order
703 CollectionUtils.sortIfNotEmpty(classpathElts, new Comparator<SimpleEntry<String, ClasspathElement>>() {
704 @Override
705 public int compare(final SimpleEntry<String, ClasspathElement> o1,
706 final SimpleEntry<String, ClasspathElement> o2) {
707 return o1.getKey().compareTo(o2.getKey());
708 }
709 });
710 // Find any nesting of elements within other elements
711 for (int i = 0; i < classpathElts.size(); i++) {
712 // See if each classpath element is a prefix of any others (if so, they will immediately follow
713 // in lexicographic order)
714 final SimpleEntry<String, ClasspathElement> ei = classpathElts.get(i);
715 final String basePath = ei.getKey();
716 final int basePathLen = basePath.length();
717 for (int j = i + 1; j < classpathElts.size(); j++) {
718 final SimpleEntry<String, ClasspathElement> ej = classpathElts.get(j);
719 final String comparePath = ej.getKey();
720 final int comparePathLen = comparePath.length();
721 boolean foundNestedClasspathRoot = false;
722 if (comparePath.startsWith(basePath) && comparePathLen > basePathLen) {
723 // Require a separator after the prefix
724 final char nextChar = comparePath.charAt(basePathLen);
725 if (nextChar == '/' || nextChar == '!') {
726 // basePath is a path prefix of comparePath. Ensure that the nested classpath does
727 // not contain another '!' zip-separator (since classpath scanning does not recurse
728 // to jars-within-jars unless they are explicitly listed on the classpath)
729 final String nestedClasspathRelativePath = comparePath.substring(basePathLen + 1);
730 if (nestedClasspathRelativePath.indexOf('!') < 0) {
731 // Found a nested classpath root
732 foundNestedClasspathRoot = true;
733 // Store link from prefix element to nested elements
734 final ClasspathElement baseElement = ei.getValue();
735 if (baseElement.nestedClasspathRootPrefixes == null) {
736 baseElement.nestedClasspathRootPrefixes = new ArrayList<>();
737 }
738 baseElement.nestedClasspathRootPrefixes.add(nestedClasspathRelativePath + "/");
739 if (log != null) {
740 log.log(basePath + " is a prefix of the nested element " + comparePath);
741 }
742 }
743 }
744 }
745 if (!foundNestedClasspathRoot) {
746 // After the first non-match, there can be no more prefix matches in the sorted order
747 break;
748 }
749 }
750 }
751 }
752
753 /**
754 * Find classpath elements whose path is a prefix of another classpath element, and record the nesting.
755 *
756 * @param finalTraditionalClasspathEltOrder
757 * the final traditional classpath elt order
758 * @param classpathFinderLog
759 * the classpath finder log
760 */
761 private void preprocessClasspathElementsByType(final List<ClasspathElement> finalTraditionalClasspathEltOrder,
762 final LogNode classpathFinderLog) {
763 final List<SimpleEntry<String, ClasspathElement>> classpathEltDirs = new ArrayList<>();
764 final List<SimpleEntry<String, ClasspathElement>> classpathEltZips = new ArrayList<>();
765 for (final ClasspathElement classpathElt : finalTraditionalClasspathEltOrder) {
766 if (classpathElt instanceof ClasspathElementDir) {
767 // Separate out ClasspathElementDir elements from other types
768 classpathEltDirs.add(
769 new SimpleEntry<>(((ClasspathElementDir) classpathElt).getFile().getPath(), classpathElt));
770
771 } else if (classpathElt instanceof ClasspathElementZip) {
772 // Separate out ClasspathElementZip elements from other types
773 final ClasspathElementZip classpathEltZip = (ClasspathElementZip) classpathElt;
774 classpathEltZips.add(new SimpleEntry<>(classpathEltZip.getZipFilePath(), classpathElt));
775
776 // Handle module-related manifest entries
777 if (classpathEltZip.logicalZipFile != null) {
778 // From JEP 261:
779 // "A <module>/<package> pair in the value of an Add-Exports attribute has the same
780 // meaning as the command-line option --add-exports <module>/<package>=ALL-UNNAMED.
781 // A <module>/<package> pair in the value of an Add-Opens attribute has the same
782 // meaning as the command-line option --add-opens <module>/<package>=ALL-UNNAMED."
783 if (classpathEltZip.logicalZipFile.addExportsManifestEntryValue != null) {
784 for (final String addExports : JarUtils.smartPathSplit(
785 classpathEltZip.logicalZipFile.addExportsManifestEntryValue, ' ', scanSpec)) {
786 scanSpec.modulePathInfo.addExports.add(addExports + "=ALL-UNNAMED");
787 }
788 }
789 if (classpathEltZip.logicalZipFile.addOpensManifestEntryValue != null) {
790 for (final String addOpens : JarUtils.smartPathSplit(
791 classpathEltZip.logicalZipFile.addOpensManifestEntryValue, ' ', scanSpec)) {
792 scanSpec.modulePathInfo.addOpens.add(addOpens + "=ALL-UNNAMED");
793 }
794 }
795 // Retrieve Automatic-Module-Name manifest entry, if present
796 if (classpathEltZip.logicalZipFile.automaticModuleNameManifestEntryValue != null) {
797 classpathEltZip.moduleNameFromManifestFile = //
798 classpathEltZip.logicalZipFile.automaticModuleNameManifestEntryValue;
799 }
800 }
801 }
802 // (Ignore ClasspathElementModule, no preprocessing to perform)
803 }
804 // Find nested classpath elements (writes to ClasspathElement#nestedClasspathRootPrefixes)
805 findNestedClasspathElements(classpathEltDirs, classpathFinderLog);
806 findNestedClasspathElements(classpathEltZips, classpathFinderLog);
807 }
808
809 // -------------------------------------------------------------------------------------------------------------
810
811 /**
812 * Perform classpath masking of classfiles. If the same relative classfile path occurs multiple times in the
813 * classpath, causes the second and subsequent occurrences to be ignored (removed).
814 *
815 * @param classpathElementOrder
816 * the classpath element order
817 * @param maskLog
818 * the mask log
819 */
820 private void maskClassfiles(final List<ClasspathElement> classpathElementOrder, final LogNode maskLog) {
821 final Set<String> whitelistedClasspathRelativePathsFound = new HashSet<>();
822 for (int classpathIdx = 0; classpathIdx < classpathElementOrder.size(); classpathIdx++) {
823 final ClasspathElement classpathElement = classpathElementOrder.get(classpathIdx);
824 classpathElement.maskClassfiles(classpathIdx, whitelistedClasspathRelativePathsFound, maskLog);
825 }
826 if (maskLog != null) {
827 maskLog.addElapsedTime();
828 }
829 }
830
831 // -------------------------------------------------------------------------------------------------------------
832
833 /**
834 * Scan the classpath and/or visible modules.
835 *
836 * @param finalClasspathEltOrder
837 * the final classpath elt order
838 * @param finalClasspathEltOrderStrs
839 * the final classpath elt order strs
840 * @param classLoaderOrderRespectingParentDelegation
841 * the environment classloader order, respecting parent-first or parent-last delegation order
842 * @return the scan result
843 * @throws InterruptedException
844 * if the scan was interrupted
845 * @throws ExecutionException
846 * if the scan threw an uncaught exception
847 */
848 private ScanResult performScan(final List<ClasspathElement> finalClasspathEltOrder,
849 final List<String> finalClasspathEltOrderStrs,
850 final ClassLoader[] classLoaderOrderRespectingParentDelegation)
851 throws InterruptedException, ExecutionException {
852 // Mask classfiles (remove any classfile resources that are shadowed by an earlier definition
853 // of the same class)
854 if (scanSpec.enableClassInfo) {
855 maskClassfiles(finalClasspathEltOrder,
856 topLevelLog == null ? null : topLevelLog.log("Masking classfiles"));
857 }
858
859 // Merge the file-to-timestamp maps across all classpath elements
860 final Map<File, Long> fileToLastModified = new HashMap<>();
861 for (final ClasspathElement classpathElement : finalClasspathEltOrder) {
862 fileToLastModified.putAll(classpathElement.fileToLastModified);
863 }
864
865 // Scan classfiles, if scanSpec.enableClassInfo is true.
866 // (classNameToClassInfo is a ConcurrentHashMap because it can be modified by
867 // ArrayTypeSignature.getArrayClassInfo() after scanning is complete)
868 final Map<String, ClassInfo> classNameToClassInfo = new ConcurrentHashMap<>();
869 final Map<String, PackageInfo> packageNameToPackageInfo = new HashMap<>();
870 final Map<String, ModuleInfo> moduleNameToModuleInfo = new HashMap<>();
871 if (scanSpec.enableClassInfo) {
872 // Get whitelisted classfile order
873 final List<ClassfileScanWorkUnit> classfileScanWorkItems = new ArrayList<>();
874 final Set<String> whitelistedClassNamesFound = new HashSet<String>();
875 for (final ClasspathElement classpathElement : finalClasspathEltOrder) {
876 // Get classfile scan order across all classpath elements
877 for (final Resource resource : classpathElement.whitelistedClassfileResources) {
878 // Create a set of names of all whitelisted classes found in classpath element paths,
879 // and double-check that a class is not going to be scanned twice
880 final String className = JarUtils.classfilePathToClassName(resource.getPath());
881 if (!whitelistedClassNamesFound.add(className) && !className.equals("module-info")
882 && !className.equals("package-info") && !className.endsWith(".package-info")) {
883 // The class should not be scheduled more than once for scanning, since classpath
884 // masking was already applied
885 throw new IllegalArgumentException("Class " + className
886 + " should not have been scheduled more than once for scanning due to classpath"
887 + " masking -- please report this bug at:"
888 + " https://github.com/classgraph/classgraph/issues");
889 }
890 // Schedule class for scanning
891 classfileScanWorkItems
892 .add(new ClassfileScanWorkUnit(classpathElement, resource, /* isExternal = */ false));
893 }
894 }
895
896 // Scan classfiles in parallel
897 final Queue<Classfile> scannedClassfiles = new ConcurrentLinkedQueue<>();
898 final ClassfileScannerWorkUnitProcessor classfileWorkUnitProcessor = //
899 new ClassfileScannerWorkUnitProcessor(scanSpec, finalClasspathEltOrder,
900 Collections.unmodifiableSet(whitelistedClassNamesFound), scannedClassfiles);
901 processWorkUnits(classfileScanWorkItems,
902 topLevelLog == null ? null : topLevelLog.log("Scanning classfiles"),
903 classfileWorkUnitProcessor);
904
905 // Link the Classfile objects to produce ClassInfo objects. This needs to be done from a single thread.
906 final LogNode linkLog = topLevelLog == null ? null : topLevelLog.log("Linking related classfiles");
907 while (!scannedClassfiles.isEmpty()) {
908 final Classfile c = scannedClassfiles.remove();
909 c.link(classNameToClassInfo, packageNameToPackageInfo, moduleNameToModuleInfo);
910 }
911
912 // Uncomment the following code to create placeholder external classes for any classes
913 // referenced in type descriptors or type signatures, so that a ClassInfo object can be
914 // obtained for those class references. This will cause all type descriptors and type
915 // signatures to be parsed, and class names extracted from them. This will add some
916 // overhead to the scanning time, and the only benefit is that
917 // ClassRefTypeSignature.getClassInfo() and AnnotationClassRef.getClassInfo() will never
918 // return null, since all external classes found in annotation class refs will have a
919 // placeholder ClassInfo object created for them. This is obscure enough that it is
920 // probably not worth slowing down scanning for all other usecases, by forcibly parsing
921 // all type descriptors and type signatures before returning the ScanResult.
922 // With this code commented out, type signatures and type descriptors are only parsed
923 // lazily, on demand.
924
925 // final Set<String> referencedClassNames = new HashSet<>();
926 // for (final ClassInfo classInfo : classNameToClassInfo.values()) {
927 // classInfo.findReferencedClassNames(referencedClassNames);
928 // }
929 // for (final String referencedClass : referencedClassNames) {
930 // ClassInfo.getOrCreateClassInfo(referencedClass, /* modifiers = */ 0, scanSpec,
931 // classNameToClassInfo);
932 // }
933
934 if (linkLog != null) {
935 linkLog.addElapsedTime();
936 }
937 } else {
938 if (topLevelLog != null) {
939 topLevelLog.log("Classfile scanning is disabled");
940 }
941 }
942
943 // Return a new ScanResult
944 return new ScanResult(scanSpec, finalClasspathEltOrder, finalClasspathEltOrderStrs,
945 classLoaderOrderRespectingParentDelegation, classNameToClassInfo, packageNameToPackageInfo,
946 moduleNameToModuleInfo, fileToLastModified, nestedJarHandler, topLevelLog);
947 }
948
949 // -------------------------------------------------------------------------------------------------------------
950
951 /**
952 * Open each of the classpath elements, looking for additional child classpath elements that need scanning (e.g.
953 * {@code Class-Path} entries in jar manifest files), then perform the scan if {@link ScanSpec#performScan} is
954 * true, or just get the classpath if {@link ScanSpec#performScan} is false.
955 *
956 * @return the scan result
957 * @throws InterruptedException
958 * if the scan was interrupted
959 * @throws ExecutionException
960 * if a worker threw an uncaught exception
961 */
962 private ScanResult openClasspathElementsThenScan() throws InterruptedException, ExecutionException {
963 // Get order of elements in traditional classpath
964 final List<ClasspathEntryWorkUnit> rawClasspathEntryWorkUnits = new ArrayList<>();
965 for (final ClasspathElementAndClassLoader rawClasspathEntry : classpathFinder.getClasspathOrder()
966 .getOrder()) {
967 rawClasspathEntryWorkUnits
968 .add(new ClasspathEntryWorkUnit(rawClasspathEntry, /* parentClasspathElement = */ null,
969 /* orderWithinParentClasspathElement = */ rawClasspathEntryWorkUnits.size()));
970 }
971
972 // In parallel, create a ClasspathElement singleton for each classpath element, then call open()
973 // on each ClasspathElement object, which in the case of jarfiles will cause LogicalZipFile instances
974 // to be created for each (possibly nested) jarfile, then will read the manifest file and zip entries.
975 final Set<ClasspathElement> openedClasspathEltsSet = Collections
976 .newSetFromMap(new ConcurrentHashMap<ClasspathElement, Boolean>());
977 final Queue<Entry<Integer, ClasspathElement>> toplevelClasspathEltOrder = new ConcurrentLinkedQueue<>();
978 processWorkUnits(rawClasspathEntryWorkUnits,
979 topLevelLog == null ? null : topLevelLog.log("Opening classpath elements"),
980 newClasspathEntryWorkUnitProcessor(openedClasspathEltsSet, toplevelClasspathEltOrder));
981
982 // Determine total ordering of classpath elements, inserting jars referenced in manifest Class-Path
983 // entries in-place into the ordering, if they haven't been listed earlier in the classpath already.
984 final List<ClasspathElement> classpathEltOrder = findClasspathOrder(openedClasspathEltsSet,
985 toplevelClasspathEltOrder);
986
987 // Find classpath elements that are path prefixes of other classpath elements, and for
988 // ClasspathElementZip, get module-related manifest entry values
989 preprocessClasspathElementsByType(classpathEltOrder,
990 topLevelLog == null ? null : topLevelLog.log("Finding nested classpath elements"));
991
992 // Order modules before classpath elements from traditional classpath
993 final LogNode classpathOrderLog = topLevelLog == null ? null
994 : topLevelLog.log("Final classpath element order:");
995 final int numElts = moduleOrder.size() + classpathEltOrder.size();
996 final List<ClasspathElement> finalClasspathEltOrder = new ArrayList<>(numElts);
997 final List<String> finalClasspathEltOrderStrs = new ArrayList<>(numElts);
998 int classpathOrderIdx = 0;
999 for (final ClasspathElementModule classpathElt : moduleOrder) {
1000 classpathElt.classpathElementIdx = classpathOrderIdx++;
1001 finalClasspathEltOrder.add(classpathElt);
1002 finalClasspathEltOrderStrs.add(classpathElt.toString());
1003 if (classpathOrderLog != null) {
1004 final ModuleRef moduleRef = classpathElt.getModuleRef();
1005 classpathOrderLog.log(moduleRef.toString());
1006 }
1007 }
1008 for (final ClasspathElement classpathElt : classpathEltOrder) {
1009 classpathElt.classpathElementIdx = classpathOrderIdx++;
1010 finalClasspathEltOrder.add(classpathElt);
1011 finalClasspathEltOrderStrs.add(classpathElt.toString());
1012 if (classpathOrderLog != null) {
1013 classpathOrderLog.log(classpathElt.toString());
1014 }
1015 }
1016
1017 // In parallel, scan paths within each classpath element, comparing them against whitelist/blacklist
1018 processWorkUnits(finalClasspathEltOrder,
1019 topLevelLog == null ? null : topLevelLog.log("Scanning classpath elements"),
1020 new WorkUnitProcessor<ClasspathElement>() {
1021 @Override
1022 public void processWorkUnit(final ClasspathElement classpathElement,
1023 final WorkQueue<ClasspathElement> workQueueIgnored, final LogNode pathScanLog)
1024 throws InterruptedException {
1025 // Scan the paths within the classpath element
1026 classpathElement.scanPaths(pathScanLog);
1027 }
1028 });
1029
1030 // Filter out classpath elements that do not contain required whitelisted paths.
1031 List<ClasspathElement> finalClasspathEltOrderFiltered = finalClasspathEltOrder;
1032 if (!scanSpec.classpathElementResourcePathWhiteBlackList.whitelistIsEmpty()) {
1033 finalClasspathEltOrderFiltered = new ArrayList<>(finalClasspathEltOrder.size());
1034 for (final ClasspathElement classpathElement : finalClasspathEltOrder) {
1035 if (classpathElement.containsSpecificallyWhitelistedClasspathElementResourcePath) {
1036 finalClasspathEltOrderFiltered.add(classpathElement);
1037 }
1038 }
1039 }
1040
1041 if (performScan) {
1042 // Scan classpath / modules, producing a ScanResult.
1043 return performScan(finalClasspathEltOrderFiltered, finalClasspathEltOrderStrs,
1044 classLoaderOrderRespectingParentDelegation);
1045 } else {
1046 // Only getting classpath -- return a placeholder ScanResult to hold classpath elements
1047 if (topLevelLog != null) {
1048 topLevelLog.log("Only returning classpath elements (not performing a scan)");
1049 }
1050 return new ScanResult(scanSpec, finalClasspathEltOrderFiltered, finalClasspathEltOrderStrs,
1051 classLoaderOrderRespectingParentDelegation, /* classNameToClassInfo = */ null,
1052 /* packageNameToPackageInfo = */ null, /* moduleNameToModuleInfo = */ null,
1053 /* fileToLastModified = */ null, nestedJarHandler, topLevelLog);
1054 }
1055 }
1056
1057 // -------------------------------------------------------------------------------------------------------------
1058
1059 /**
1060 * Determine the unique ordered classpath elements, and run a scan looking for file or classfile matches if
1061 * necessary.
1062 *
1063 * @return the scan result
1064 * @throws InterruptedException
1065 * if scanning was interrupted
1066 * @throws CancellationException
1067 * if scanning was cancelled
1068 * @throws ExecutionException
1069 * if a worker threw an uncaught exception
1070 */
1071 @Override
1072 public ScanResult call() throws InterruptedException, CancellationException, ExecutionException {
1073 ScanResult scanResult = null;
1074 final long scanStart = System.currentTimeMillis();
1075 boolean removeTemporaryFilesAfterScan = scanSpec.removeTemporaryFilesAfterScan;
1076 try {
1077 // Perform the scan
1078 scanResult = openClasspathElementsThenScan();
1079
1080 // Log total time after scan completes, and flush log
1081 if (topLevelLog != null) {
1082 topLevelLog.log("~",
1083 String.format("Total time: %.3f sec", (System.currentTimeMillis() - scanStart) * .001));
1084 topLevelLog.flush();
1085 }
1086
1087 // Call the ScanResultProcessor, if one was provided
1088 if (scanResultProcessor != null) {
1089 try {
1090 scanResultProcessor.processScanResult(scanResult);
1091 } finally {
1092 scanResult.close();
1093 }
1094 }
1095
1096 } catch (final Throwable e) {
1097 if (topLevelLog != null) {
1098 topLevelLog.log("~",
1099 e instanceof InterruptedException || e instanceof CancellationException
1100 ? "Scan interrupted or canceled"
1101 : e instanceof ExecutionException || e instanceof RuntimeException
1102 ? "Uncaught exception during scan"
1103 : e.getMessage(),
1104 InterruptionChecker.getCause(e));
1105 // Flush the log
1106 topLevelLog.flush();
1107 }
1108
1109 // Since an exception was thrown, remove temporary files
1110 removeTemporaryFilesAfterScan = true;
1111
1112 // Stop any running threads (should not be needed, threads should already be quiescent)
1113 interruptionChecker.interrupt();
1114
1115 if (failureHandler == null) {
1116 // If there is no failure handler set, re-throw the exception
1117 throw e;
1118 } else {
1119 // Otherwise, call the failure handler
1120 try {
1121 failureHandler.onFailure(e);
1122 } catch (final Exception f) {
1123 // The failure handler failed
1124 if (topLevelLog != null) {
1125 topLevelLog.log("~", "The failure handler threw an exception:", f);
1126 topLevelLog.flush();
1127 }
1128 // Group the two exceptions into one, using the suppressed exception mechanism
1129 // to show the scan exception below the failure handler exception
1130 final ExecutionException failureHandlerException = new ExecutionException(
1131 "Exception while calling failure handler", f);
1132 failureHandlerException.addSuppressed(e);
1133 // Throw a new ExecutionException (although this will probably be ignored,
1134 // since any job with a FailureHandler was started with ExecutorService::execute
1135 // rather than ExecutorService::submit)
1136 throw failureHandlerException;
1137 }
1138 }
1139
1140 } finally {
1141 if (removeTemporaryFilesAfterScan) {
1142 // If removeTemporaryFilesAfterScan was set, remove temp files and close resources,
1143 // zipfiles and modules
1144 nestedJarHandler.close(topLevelLog);
1145 }
1146 }
1147 return scanResult;
1148 }
1149 }
1150