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