1package com.github.javaparser.utils;
2
3import com.github.javaparser.JavaParser;
4import com.github.javaparser.ParseProblemException;
5import com.github.javaparser.ParseResult;
6import com.github.javaparser.ParserConfiguration;
7import com.github.javaparser.ast.CompilationUnit;
8import com.github.javaparser.printer.PrettyPrinter;
9
10import java.io.IOException;
11import java.nio.file.FileVisitResult;
12import java.nio.file.Files;
13import java.nio.file.Path;
14import java.nio.file.SimpleFileVisitor;
15import java.nio.file.attribute.BasicFileAttributes;
16import java.util.ArrayList;
17import java.util.List;
18import java.util.Map;
19import java.util.concurrent.ConcurrentHashMap;
20import java.util.concurrent.ForkJoinPool;
21import java.util.concurrent.RecursiveAction;
22import java.util.function.Function;
23import java.util.regex.Pattern;
24import java.util.stream.Collectors;
25
26import static com.github.javaparser.ParseStart.COMPILATION_UNIT;
27import static com.github.javaparser.Providers.provider;
28import static com.github.javaparser.utils.CodeGenerationUtils.fileInPackageRelativePath;
29import static com.github.javaparser.utils.CodeGenerationUtils.packageAbsolutePath;
30import static com.github.javaparser.utils.SourceRoot.Callback.Result.SAVE;
31import static com.github.javaparser.utils.Utils.assertNotNull;
32import static java.nio.file.FileVisitResult.CONTINUE;
33import 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 */
44public 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         */
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
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
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
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
469    public Path getRoot() {
470        return root;
471    }
472
473    /**
474     * @deprecated store ParserConfiguration now
475     */
476    @Deprecated
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
487    public SourceRoot setJavaParser(JavaParser javaParser) {
488        assertNotNull(javaParser);
489        this.parserConfiguration = javaParser.getParserConfiguration();
490        return this;
491    }
492
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     */
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     */
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     */
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
534        ParallelParse(Path path, VisitFileCallback callback) {
535            this.path = path;
536            this.callback = callback;
537        }
538
539        @Override
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 {
574            FileVisitResult process(Path file, BasicFileAttributes attrs);
575        }
576    }
577}
578