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