• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.github.javaparser.utils;
2 
3 import com.github.javaparser.JavaParser;
4 import com.github.javaparser.ParseProblemException;
5 import com.github.javaparser.ParseResult;
6 import com.github.javaparser.ParserConfiguration;
7 import com.github.javaparser.ast.CompilationUnit;
8 import com.github.javaparser.printer.PrettyPrinter;
9 
10 import java.io.IOException;
11 import java.nio.file.FileVisitResult;
12 import java.nio.file.Files;
13 import java.nio.file.Path;
14 import java.nio.file.SimpleFileVisitor;
15 import java.nio.file.attribute.BasicFileAttributes;
16 import java.util.ArrayList;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.ForkJoinPool;
21 import java.util.concurrent.RecursiveAction;
22 import java.util.function.Function;
23 import java.util.regex.Pattern;
24 import java.util.stream.Collectors;
25 
26 import static com.github.javaparser.ParseStart.COMPILATION_UNIT;
27 import static com.github.javaparser.Providers.provider;
28 import static com.github.javaparser.utils.CodeGenerationUtils.fileInPackageRelativePath;
29 import static com.github.javaparser.utils.CodeGenerationUtils.packageAbsolutePath;
30 import static com.github.javaparser.utils.SourceRoot.Callback.Result.SAVE;
31 import static com.github.javaparser.utils.Utils.assertNotNull;
32 import static java.nio.file.FileVisitResult.CONTINUE;
33 import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
34 
35 /**
36  * A collection of Java source files located in one directory and its subdirectories on the file system. Files can be
37  * parsed and written back one by one or all together. <b>Note that</b> the internal cache used is thread-safe.
38  * <ul>
39  * <li>methods called "tryToParse..." will return their result inside a "ParseResult", which supports parse successes and failures.</li>
40  * <li>methods called "parse..." will return "CompilationUnit"s. If a file fails to parse, an exception is thrown.</li>
41  * <li>methods ending in "...Parallelized" will speed up parsing by using multiple threads.</li>
42  * </ul>
43  */
44 public class SourceRoot {
45     @FunctionalInterface
46     public interface Callback {
47         enum Result {
48             SAVE, DONT_SAVE
49         }
50 
51         /**
52          * @param localPath the path to the file that was parsed, relative to the source root path.
53          * @param absolutePath the absolute path to the file that was parsed.
54          * @param result the result of of parsing the file.
55          */
process(Path localPath, Path absolutePath, ParseResult<CompilationUnit> result)56         Result process(Path localPath, Path absolutePath, ParseResult<CompilationUnit> result);
57     }
58 
59     private final Path root;
60     private final Map<Path, ParseResult<CompilationUnit>> cache = new ConcurrentHashMap<>();
61     private ParserConfiguration parserConfiguration = new ParserConfiguration();
62     private Function<CompilationUnit, String> printer = new PrettyPrinter()::print;
63     private static final Pattern JAVA_IDENTIFIER = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*");
64 
SourceRoot(Path root)65     public SourceRoot(Path root) {
66         assertNotNull(root);
67         if (!Files.isDirectory(root)) {
68             throw new IllegalArgumentException("Only directories are allowed as root path!");
69         }
70         this.root = root.normalize();
71         Log.info("New source root at \"%s\"", this.root);
72     }
73 
SourceRoot(Path root, ParserConfiguration parserConfiguration)74     public SourceRoot(Path root, ParserConfiguration parserConfiguration) {
75         this(root);
76         setParserConfiguration(parserConfiguration);
77     }
78 
79     /**
80      * Tries to parse a .java files under the source root and returns the ParseResult. It keeps track of the parsed file
81      * so you can write it out with the saveAll() call. Note that the cache grows with every file parsed, so if you
82      * don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is useful) you
83      * might want to use the parse method with a callback.
84      *
85      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
86      * @deprecated pass ParserConfiguration instead of JavaParser
87      */
88     @Deprecated
tryToParse(String startPackage, String filename, JavaParser javaParser)89     public ParseResult<CompilationUnit> tryToParse(String startPackage, String filename, JavaParser javaParser)
90             throws IOException {
91         return tryToParse(startPackage, filename, javaParser.getParserConfiguration());
92     }
93 
94     /**
95      * Tries to parse a .java files under the source root and returns the ParseResult. It keeps track of the parsed file
96      * so you can write it out with the saveAll() call. Note that the cache grows with every file parsed, so if you
97      * don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is useful) you
98      * might want to use the parse method with a callback.
99      *
100      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
101      */
tryToParse(String startPackage, String filename, ParserConfiguration configuration)102     public ParseResult<CompilationUnit> tryToParse(String startPackage, String filename, ParserConfiguration configuration) throws IOException {
103         assertNotNull(startPackage);
104         assertNotNull(filename);
105         final Path relativePath = fileInPackageRelativePath(startPackage, filename);
106         if (cache.containsKey(relativePath)) {
107             Log.trace("Retrieving cached %s", relativePath);
108             return cache.get(relativePath);
109         }
110         final Path path = root.resolve(relativePath);
111         Log.trace("Parsing %s", path);
112         final ParseResult<CompilationUnit> result = new JavaParser(configuration)
113                 .parse(COMPILATION_UNIT, provider(path));
114         result.getResult().ifPresent(cu -> cu.setStorage(path));
115         cache.put(relativePath, result);
116         return result;
117     }
118 
119     /**
120      * Tries to parse a .java files under the source root and returns the ParseResult. It keeps track of the parsed file
121      * so you can write it out with the saveAll() call. Note that the cache grows with every file parsed, so if you
122      * don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is useful) you
123      * might want to use the parse method with a callback.
124      *
125      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
126      */
tryToParse(String startPackage, String filename)127     public ParseResult<CompilationUnit> tryToParse(String startPackage, String filename) throws IOException {
128         return tryToParse(startPackage, filename, parserConfiguration);
129     }
130 
131     /**
132      * Tries to parse all .java files in a package recursively, and returns all files ever parsed with this source root.
133      * It keeps track of all parsed files so you can write them out with a single saveAll() call. Note that the cache
134      * grows with every file parsed, so if you don't need saveAll(), or you don't ask SourceRoot to parse files multiple
135      * times (where the cache is useful) you might want to use the parse method with a callback.
136      *
137      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
138      */
tryToParse(String startPackage)139     public List<ParseResult<CompilationUnit>> tryToParse(String startPackage) throws IOException {
140         assertNotNull(startPackage);
141         logPackage(startPackage);
142         final Path path = packageAbsolutePath(root, startPackage);
143         Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
144             @Override
145             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
146                 if (!attrs.isDirectory() && file.toString().endsWith(".java")) {
147                     Path relative = root.relativize(file.getParent());
148                     tryToParse(relative.toString(), file.getFileName().toString());
149                 }
150                 return CONTINUE;
151             }
152 
153             @Override
154             public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
155                 return isSensibleDirectoryToEnter(dir) ? CONTINUE : SKIP_SUBTREE;
156             }
157         });
158         return getCache();
159     }
160 
isSensibleDirectoryToEnter(Path dir)161     private static boolean isSensibleDirectoryToEnter(Path dir) throws IOException {
162         final String dirToEnter = dir.getFileName().toString();
163         final boolean directoryIsAValidJavaIdentifier = JAVA_IDENTIFIER.matcher(dirToEnter).matches();
164         if (Files.isHidden(dir) || !directoryIsAValidJavaIdentifier) {
165             Log.trace("Not processing directory \"%s\"", dirToEnter);
166             return false;
167         }
168         return true;
169     }
170 
171     /**
172      * Tries to parse all .java files under the source root recursively, and returns all files ever parsed with this
173      * source root. It keeps track of all parsed files so you can write them out with a single saveAll() call. Note that
174      * the cache grows with every file parsed, so if you don't need saveAll(), or you don't ask SourceRoot to parse
175      * files multiple times (where the cache is useful) you might want to use the parse method with a callback.
176      */
tryToParse()177     public List<ParseResult<CompilationUnit>> tryToParse() throws IOException {
178         return tryToParse("");
179     }
180 
181     /**
182      * Tries to parse all .java files in a package recursively using multiple threads, and returns all files ever parsed
183      * with this source root. A new thread is forked each time a new directory is visited and is responsible for parsing
184      * all .java files in that directory. <b>Note that</b> to ensure thread safety, a new parser instance is created for
185      * every file with the internal parser's (i.e. {@link #setJavaParser}) configuration. It keeps track of all parsed
186      * files so you can write them out with a single saveAll() call. Note that the cache grows with every file parsed,
187      * so if you don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is
188      * useful) you might want to use the parse method with a callback.
189      *
190      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
191      */
tryToParseParallelized(String startPackage)192     public List<ParseResult<CompilationUnit>> tryToParseParallelized(String startPackage) {
193         assertNotNull(startPackage);
194         logPackage(startPackage);
195         final Path path = packageAbsolutePath(root, startPackage);
196         ParallelParse parse = new ParallelParse(path, (file, attrs) -> {
197             if (!attrs.isDirectory() && file.toString().endsWith(".java")) {
198                 Path relative = root.relativize(file.getParent());
199                 try {
200                     tryToParse(
201                             relative.toString(),
202                             file.getFileName().toString(),
203                             parserConfiguration);
204                 } catch (IOException e) {
205                     Log.error(e);
206                 }
207             }
208             return CONTINUE;
209         });
210         ForkJoinPool pool = new ForkJoinPool();
211         pool.invoke(parse);
212         return getCache();
213     }
214 
215     /**
216      * Tries to parse all .java files under the source root recursively using multiple threads, and returns all files
217      * ever parsed with this source root. A new thread is forked each time a new directory is visited and is responsible
218      * for parsing all .java files in that directory. <b>Note that</b> to ensure thread safety, a new parser instance is
219      * created for every file with the internal parser's (i.e. {@link #setJavaParser}) configuration. It keeps track of
220      * all parsed files so you can write them out with a single saveAll() call. Note that the cache grows with every
221      * file parsed, so if you don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the
222      * cache is useful) you might want to use the parse method with a callback.
223      */
tryToParseParallelized()224     public List<ParseResult<CompilationUnit>> tryToParseParallelized() throws IOException {
225         return tryToParseParallelized("");
226     }
227 
228     /**
229      * Parses a .java files under the source root and returns its CompilationUnit. It keeps track of the parsed file so
230      * you can write it out with the saveAll() call. Note that the cache grows with every file parsed, so if you don't
231      * need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is useful) you might
232      * want to use the parse method with a callback.
233      *
234      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
235      * @throws ParseProblemException when something went wrong.
236      */
parse(String startPackage, String filename)237     public CompilationUnit parse(String startPackage, String filename) {
238         assertNotNull(startPackage);
239         assertNotNull(filename);
240         try {
241             final ParseResult<CompilationUnit> result = tryToParse(startPackage, filename);
242             if (result.isSuccessful()) {
243                 return result.getResult().get();
244             }
245             throw new ParseProblemException(result.getProblems());
246         } catch (IOException e) {
247             throw new ParseProblemException(e);
248         }
249     }
250 
251     /**
252      * Tries to parse all .java files in a package recursively and passes them one by one to the callback. In comparison
253      * to the other parse methods, this is much more memory efficient, but saveAll() won't work.
254      *
255      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
256      * @deprecated pass ParserConfiguration instead of JavaParser
257      */
258     @Deprecated
parse(String startPackage, JavaParser javaParser, Callback callback)259     public SourceRoot parse(String startPackage, JavaParser javaParser, Callback callback) throws IOException {
260         return parse(startPackage, javaParser.getParserConfiguration(), callback);
261     }
262 
263     /**
264      * Tries to parse all .java files in a package recursively and passes them one by one to the callback. In comparison
265      * to the other parse methods, this is much more memory efficient, but saveAll() won't work.
266      *
267      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
268      */
parse(String startPackage, ParserConfiguration configuration, Callback callback)269     public SourceRoot parse(String startPackage, ParserConfiguration configuration, Callback callback) throws IOException {
270         assertNotNull(startPackage);
271         assertNotNull(configuration);
272         assertNotNull(callback);
273         logPackage(startPackage);
274         final JavaParser javaParser = new JavaParser(configuration);
275         final Path path = packageAbsolutePath(root, startPackage);
276         Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
277             @Override
278             public FileVisitResult visitFile(Path absolutePath, BasicFileAttributes attrs) throws IOException {
279                 if (!attrs.isDirectory() && absolutePath.toString().endsWith(".java")) {
280                     Path localPath = root.relativize(absolutePath);
281                     Log.trace("Parsing %s", localPath);
282                     final ParseResult<CompilationUnit> result = javaParser.parse(COMPILATION_UNIT,
283                             provider(absolutePath));
284                     result.getResult().ifPresent(cu -> cu.setStorage(absolutePath));
285                     if (callback.process(localPath, absolutePath, result) == SAVE) {
286                         if (result.getResult().isPresent()) {
287                             save(result.getResult().get(), path);
288                         }
289                     }
290                 }
291                 return CONTINUE;
292             }
293 
294             @Override
295             public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
296                 return isSensibleDirectoryToEnter(dir) ? CONTINUE : SKIP_SUBTREE;
297             }
298         });
299         return this;
300     }
301 
logPackage(String startPackage)302     private void logPackage(String startPackage) {
303         if (startPackage.isEmpty()) {
304             return;
305         }
306         Log.info("Parsing package \"%s\"", startPackage);
307     }
308 
309     /**
310      * Tries to parse all .java files in a package recursively using multiple threads, and passes them one by one to the
311      * callback. A new thread is forked each time a new directory is visited and is responsible for parsing all .java
312      * files in that directory. <b>Note that</b> the provided {@link Callback} code must be made thread-safe. <b>Note
313      * that</b> to ensure thread safety, a new parser instance is created for every file with the provided {@link
314      * ParserConfiguration}. In comparison to the other parse methods, this is much more memory efficient, but saveAll()
315      * won't work.
316      *
317      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
318      */
parseParallelized(String startPackage, ParserConfiguration configuration, Callback callback)319     public SourceRoot parseParallelized(String startPackage, ParserConfiguration configuration, Callback callback) {
320         assertNotNull(startPackage);
321         assertNotNull(configuration);
322         assertNotNull(callback);
323         logPackage(startPackage);
324         final Path path = packageAbsolutePath(root, startPackage);
325         ParallelParse parse = new ParallelParse(path, (file, attrs) -> {
326             if (!attrs.isDirectory() && file.toString().endsWith(".java")) {
327                 Path localPath = root.relativize(file);
328                 Log.trace("Parsing %s", localPath);
329                 try {
330                     ParseResult<CompilationUnit> result = new JavaParser(configuration)
331                             .parse(COMPILATION_UNIT, provider(file));
332                     result.getResult().ifPresent(cu -> cu.setStorage(file));
333                     if (callback.process(localPath, file, result) == SAVE) {
334                         if (result.getResult().isPresent()) {
335                             save(result.getResult().get(), path);
336                         }
337                     }
338                 } catch (IOException e) {
339                     Log.error(e);
340                 }
341             }
342             return CONTINUE;
343         });
344         ForkJoinPool pool = new ForkJoinPool();
345         pool.invoke(parse);
346         return this;
347     }
348 
349     /**
350      * Tries to parse all .java files in a package recursively using multiple threads, and passes them one by one to the
351      * callback. A new thread is forked each time a new directory is visited and is responsible for parsing all .java
352      * files in that directory. <b>Note that</b> the provided {@link Callback} code must be made thread-safe. <b>Note
353      * that</b> to ensure thread safety, a new parser instance is created for every file. In comparison to the other
354      * parse methods, this is much more memory efficient, but saveAll() won't work.
355      *
356      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
357      */
parseParallelized(String startPackage, Callback callback)358     public SourceRoot parseParallelized(String startPackage, Callback callback) throws IOException {
359         return parseParallelized(startPackage, new ParserConfiguration(), callback);
360     }
361 
362     /**
363      * Tries to parse all .java files recursively using multiple threads, and passes them one by one to the callback. A
364      * new thread is forked each time a new directory is visited and is responsible for parsing all .java files in that
365      * directory. <b>Note that</b> the provided {@link Callback} code must be made thread-safe. <b>Note that</b> to
366      * ensure thread safety, a new parser instance is created for every file. In comparison to the other parse methods,
367      * this is much more memory efficient, but saveAll() won't work.
368      */
parseParallelized(Callback callback)369     public SourceRoot parseParallelized(Callback callback) throws IOException {
370         return parseParallelized("", new ParserConfiguration(), callback);
371     }
372 
373     /**
374      * Add a newly created Java file to the cache of this source root. It will be saved when saveAll is called.
375      *
376      * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files.
377      */
add(String startPackage, String filename, CompilationUnit compilationUnit)378     public SourceRoot add(String startPackage, String filename, CompilationUnit compilationUnit) {
379         assertNotNull(startPackage);
380         assertNotNull(filename);
381         assertNotNull(compilationUnit);
382         Log.trace("Adding new file %s.%s", startPackage, filename);
383         final Path path = fileInPackageRelativePath(startPackage, filename);
384         final ParseResult<CompilationUnit> parseResult = new ParseResult<>(
385                 compilationUnit,
386                 new ArrayList<>(),
387                 null,
388                 null);
389         cache.put(path, parseResult);
390         return this;
391     }
392 
393     /**
394      * Add a newly created Java file to the cache of this source root. It will be saved when saveAll is called. It needs
395      * to have its path set.
396      */
add(CompilationUnit compilationUnit)397     public SourceRoot add(CompilationUnit compilationUnit) {
398         assertNotNull(compilationUnit);
399         if (compilationUnit.getStorage().isPresent()) {
400             final Path path = compilationUnit.getStorage().get().getPath();
401             Log.trace("Adding new file %s", path);
402             final ParseResult<CompilationUnit> parseResult = new ParseResult<>(
403                     compilationUnit,
404                     new ArrayList<>(),
405                     null,
406                     null);
407             cache.put(path, parseResult);
408         } else {
409             throw new AssertionError("Files added with this method should have their path set.");
410         }
411         return this;
412     }
413 
414     /**
415      * Save the given compilation unit to the given path.
416      */
save(CompilationUnit cu, Path path)417     private SourceRoot save(CompilationUnit cu, Path path) {
418         assertNotNull(cu);
419         assertNotNull(path);
420         cu.setStorage(path);
421         cu.getStorage().get().save(printer);
422         return this;
423     }
424 
425     /**
426      * Save all previously parsed files back to a new path.
427      */
saveAll(Path root)428     public SourceRoot saveAll(Path root) {
429         assertNotNull(root);
430         Log.info("Saving all files (%s) to %s", cache.size(), root);
431         for (Map.Entry<Path, ParseResult<CompilationUnit>> cu : cache.entrySet()) {
432             final Path path = root.resolve(cu.getKey());
433             if (cu.getValue().getResult().isPresent()) {
434                 Log.trace("Saving %s", path);
435                 save(cu.getValue().getResult().get(), path);
436             }
437         }
438         return this;
439     }
440 
441     /**
442      * Save all previously parsed files back to where they were found.
443      */
saveAll()444     public SourceRoot saveAll() {
445         return saveAll(root);
446     }
447 
448     /**
449      * The Java files that have been parsed by this source root object, or have been added manually.
450      */
getCache()451     public List<ParseResult<CompilationUnit>> getCache() {
452         return new ArrayList<>(cache.values());
453     }
454 
455     /**
456      * The CompilationUnits of the Java files that have been parsed succesfully by this source root object, or have been
457      * added manually.
458      */
getCompilationUnits()459     public List<CompilationUnit> getCompilationUnits() {
460         return cache.values().stream()
461                 .filter(ParseResult::isSuccessful)
462                 .map(p -> p.getResult().get())
463                 .collect(Collectors.toList());
464     }
465 
466     /**
467      * The path that was passed in the constructor.
468      */
getRoot()469     public Path getRoot() {
470         return root;
471     }
472 
473     /**
474      * @deprecated store ParserConfiguration now
475      */
476     @Deprecated
getJavaParser()477     public JavaParser getJavaParser() {
478         return new JavaParser(parserConfiguration);
479     }
480 
481     /**
482      * Set the parser that is used for parsing by default.
483      *
484      * @deprecated store ParserConfiguration now
485      */
486     @Deprecated
setJavaParser(JavaParser javaParser)487     public SourceRoot setJavaParser(JavaParser javaParser) {
488         assertNotNull(javaParser);
489         this.parserConfiguration = javaParser.getParserConfiguration();
490         return this;
491     }
492 
getParserConfiguration()493     public ParserConfiguration getParserConfiguration() {
494         return parserConfiguration;
495     }
496 
497     /**
498      * Set the parser configuration that is used for parsing when no configuration is passed to a method.
499      */
setParserConfiguration(ParserConfiguration parserConfiguration)500     public SourceRoot setParserConfiguration(ParserConfiguration parserConfiguration) {
501         assertNotNull(parserConfiguration);
502         this.parserConfiguration = parserConfiguration;
503         return this;
504     }
505 
506     /**
507      * Set the printing function that transforms compilation units into a string to save.
508      */
setPrinter(Function<CompilationUnit, String> printer)509     public SourceRoot setPrinter(Function<CompilationUnit, String> printer) {
510         assertNotNull(printer);
511         this.printer = printer;
512         return this;
513     }
514 
515     /**
516      * Get the printing function.
517      */
getPrinter()518     public Function<CompilationUnit, String> getPrinter() {
519         return printer;
520     }
521 
522     /**
523      * Executes a recursive file tree walk using threads. A new thread is invoked for each new directory discovered
524      * during the walk. For each file visited, the user-provided {@link VisitFileCallback} is called with the current
525      * path and file attributes. Any shared resources accessed in a {@link VisitFileCallback} should be made
526      * thread-safe.
527      */
528     private static class ParallelParse extends RecursiveAction {
529 
530         private static final long serialVersionUID = 1L;
531         private final Path path;
532         private final VisitFileCallback callback;
533 
ParallelParse(Path path, VisitFileCallback callback)534         ParallelParse(Path path, VisitFileCallback callback) {
535             this.path = path;
536             this.callback = callback;
537         }
538 
539         @Override
compute()540         protected void compute() {
541             final List<ParallelParse> walks = new ArrayList<>();
542             try {
543                 Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
544                     @Override
545                     public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
546                         if (!SourceRoot.isSensibleDirectoryToEnter(dir)) {
547                             return SKIP_SUBTREE;
548                         }
549                         if (!dir.equals(ParallelParse.this.path)) {
550                             ParallelParse w = new ParallelParse(dir, callback);
551                             w.fork();
552                             walks.add(w);
553                             return SKIP_SUBTREE;
554                         } else {
555                             return CONTINUE;
556                         }
557                     }
558 
559                     @Override
560                     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
561                         return callback.process(file, attrs);
562                     }
563                 });
564             } catch (IOException e) {
565                 Log.error(e);
566             }
567 
568             for (ParallelParse w : walks) {
569                 w.join();
570             }
571         }
572 
573         interface VisitFileCallback {
process(Path file, BasicFileAttributes attrs)574             FileVisitResult process(Path file, BasicFileAttributes attrs);
575         }
576     }
577 }
578