diff options
Diffstat (limited to 'javaparser-core/src/main/java/com/github/javaparser/utils/SourceRoot.java')
-rw-r--r-- | javaparser-core/src/main/java/com/github/javaparser/utils/SourceRoot.java | 577 |
1 files changed, 577 insertions, 0 deletions
diff --git a/javaparser-core/src/main/java/com/github/javaparser/utils/SourceRoot.java b/javaparser-core/src/main/java/com/github/javaparser/utils/SourceRoot.java new file mode 100644 index 000000000..02a90ffec --- /dev/null +++ b/javaparser-core/src/main/java/com/github/javaparser/utils/SourceRoot.java @@ -0,0 +1,577 @@ +package com.github.javaparser.utils; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParseProblemException; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.printer.PrettyPrinter; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.RecursiveAction; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static com.github.javaparser.ParseStart.COMPILATION_UNIT; +import static com.github.javaparser.Providers.provider; +import static com.github.javaparser.utils.CodeGenerationUtils.fileInPackageRelativePath; +import static com.github.javaparser.utils.CodeGenerationUtils.packageAbsolutePath; +import static com.github.javaparser.utils.SourceRoot.Callback.Result.SAVE; +import static com.github.javaparser.utils.Utils.assertNotNull; +import static java.nio.file.FileVisitResult.CONTINUE; +import static java.nio.file.FileVisitResult.SKIP_SUBTREE; + +/** + * A collection of Java source files located in one directory and its subdirectories on the file system. Files can be + * parsed and written back one by one or all together. <b>Note that</b> the internal cache used is thread-safe. + * <ul> + * <li>methods called "tryToParse..." will return their result inside a "ParseResult", which supports parse successes and failures.</li> + * <li>methods called "parse..." will return "CompilationUnit"s. If a file fails to parse, an exception is thrown.</li> + * <li>methods ending in "...Parallelized" will speed up parsing by using multiple threads.</li> + * </ul> + */ +public class SourceRoot { + @FunctionalInterface + public interface Callback { + enum Result { + SAVE, DONT_SAVE + } + + /** + * @param localPath the path to the file that was parsed, relative to the source root path. + * @param absolutePath the absolute path to the file that was parsed. + * @param result the result of of parsing the file. + */ + Result process(Path localPath, Path absolutePath, ParseResult<CompilationUnit> result); + } + + private final Path root; + private final Map<Path, ParseResult<CompilationUnit>> cache = new ConcurrentHashMap<>(); + private ParserConfiguration parserConfiguration = new ParserConfiguration(); + private Function<CompilationUnit, String> printer = new PrettyPrinter()::print; + private static final Pattern JAVA_IDENTIFIER = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); + + public SourceRoot(Path root) { + assertNotNull(root); + if (!Files.isDirectory(root)) { + throw new IllegalArgumentException("Only directories are allowed as root path!"); + } + this.root = root.normalize(); + Log.info("New source root at \"%s\"", this.root); + } + + public SourceRoot(Path root, ParserConfiguration parserConfiguration) { + this(root); + setParserConfiguration(parserConfiguration); + } + + /** + * Tries to parse a .java files under the source root and returns the ParseResult. It keeps track of the parsed file + * so you can write it out with the saveAll() call. Note that the cache grows with every file parsed, so if you + * don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is useful) you + * might want to use the parse method with a callback. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + * @deprecated pass ParserConfiguration instead of JavaParser + */ + @Deprecated + public ParseResult<CompilationUnit> tryToParse(String startPackage, String filename, JavaParser javaParser) + throws IOException { + return tryToParse(startPackage, filename, javaParser.getParserConfiguration()); + } + + /** + * Tries to parse a .java files under the source root and returns the ParseResult. It keeps track of the parsed file + * so you can write it out with the saveAll() call. Note that the cache grows with every file parsed, so if you + * don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is useful) you + * might want to use the parse method with a callback. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + */ + public ParseResult<CompilationUnit> tryToParse(String startPackage, String filename, ParserConfiguration configuration) throws IOException { + assertNotNull(startPackage); + assertNotNull(filename); + final Path relativePath = fileInPackageRelativePath(startPackage, filename); + if (cache.containsKey(relativePath)) { + Log.trace("Retrieving cached %s", relativePath); + return cache.get(relativePath); + } + final Path path = root.resolve(relativePath); + Log.trace("Parsing %s", path); + final ParseResult<CompilationUnit> result = new JavaParser(configuration) + .parse(COMPILATION_UNIT, provider(path)); + result.getResult().ifPresent(cu -> cu.setStorage(path)); + cache.put(relativePath, result); + return result; + } + + /** + * Tries to parse a .java files under the source root and returns the ParseResult. It keeps track of the parsed file + * so you can write it out with the saveAll() call. Note that the cache grows with every file parsed, so if you + * don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is useful) you + * might want to use the parse method with a callback. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + */ + public ParseResult<CompilationUnit> tryToParse(String startPackage, String filename) throws IOException { + return tryToParse(startPackage, filename, parserConfiguration); + } + + /** + * Tries to parse all .java files in a package recursively, and returns all files ever parsed with this source root. + * It keeps track of all parsed files so you can write them out with a single saveAll() call. Note that the cache + * grows with every file parsed, so if you don't need saveAll(), or you don't ask SourceRoot to parse files multiple + * times (where the cache is useful) you might want to use the parse method with a callback. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + */ + public List<ParseResult<CompilationUnit>> tryToParse(String startPackage) throws IOException { + assertNotNull(startPackage); + logPackage(startPackage); + final Path path = packageAbsolutePath(root, startPackage); + Files.walkFileTree(path, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!attrs.isDirectory() && file.toString().endsWith(".java")) { + Path relative = root.relativize(file.getParent()); + tryToParse(relative.toString(), file.getFileName().toString()); + } + return CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return isSensibleDirectoryToEnter(dir) ? CONTINUE : SKIP_SUBTREE; + } + }); + return getCache(); + } + + private static boolean isSensibleDirectoryToEnter(Path dir) throws IOException { + final String dirToEnter = dir.getFileName().toString(); + final boolean directoryIsAValidJavaIdentifier = JAVA_IDENTIFIER.matcher(dirToEnter).matches(); + if (Files.isHidden(dir) || !directoryIsAValidJavaIdentifier) { + Log.trace("Not processing directory \"%s\"", dirToEnter); + return false; + } + return true; + } + + /** + * Tries to parse all .java files under the source root recursively, and returns all files ever parsed with this + * source root. It keeps track of all parsed files so you can write them out with a single saveAll() call. Note that + * the cache grows with every file parsed, so if you don't need saveAll(), or you don't ask SourceRoot to parse + * files multiple times (where the cache is useful) you might want to use the parse method with a callback. + */ + public List<ParseResult<CompilationUnit>> tryToParse() throws IOException { + return tryToParse(""); + } + + /** + * Tries to parse all .java files in a package recursively using multiple threads, and returns all files ever parsed + * with this source root. A new thread is forked each time a new directory is visited and is responsible for parsing + * all .java files in that directory. <b>Note that</b> to ensure thread safety, a new parser instance is created for + * every file with the internal parser's (i.e. {@link #setJavaParser}) configuration. It keeps track of all parsed + * files so you can write them out with a single saveAll() call. Note that the cache grows with every file parsed, + * so if you don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is + * useful) you might want to use the parse method with a callback. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + */ + public List<ParseResult<CompilationUnit>> tryToParseParallelized(String startPackage) { + assertNotNull(startPackage); + logPackage(startPackage); + final Path path = packageAbsolutePath(root, startPackage); + ParallelParse parse = new ParallelParse(path, (file, attrs) -> { + if (!attrs.isDirectory() && file.toString().endsWith(".java")) { + Path relative = root.relativize(file.getParent()); + try { + tryToParse( + relative.toString(), + file.getFileName().toString(), + parserConfiguration); + } catch (IOException e) { + Log.error(e); + } + } + return CONTINUE; + }); + ForkJoinPool pool = new ForkJoinPool(); + pool.invoke(parse); + return getCache(); + } + + /** + * Tries to parse all .java files under the source root recursively using multiple threads, and returns all files + * ever parsed with this source root. A new thread is forked each time a new directory is visited and is responsible + * for parsing all .java files in that directory. <b>Note that</b> to ensure thread safety, a new parser instance is + * created for every file with the internal parser's (i.e. {@link #setJavaParser}) configuration. It keeps track of + * all parsed files so you can write them out with a single saveAll() call. Note that the cache grows with every + * file parsed, so if you don't need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the + * cache is useful) you might want to use the parse method with a callback. + */ + public List<ParseResult<CompilationUnit>> tryToParseParallelized() throws IOException { + return tryToParseParallelized(""); + } + + /** + * Parses a .java files under the source root and returns its CompilationUnit. It keeps track of the parsed file so + * you can write it out with the saveAll() call. Note that the cache grows with every file parsed, so if you don't + * need saveAll(), or you don't ask SourceRoot to parse files multiple times (where the cache is useful) you might + * want to use the parse method with a callback. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + * @throws ParseProblemException when something went wrong. + */ + public CompilationUnit parse(String startPackage, String filename) { + assertNotNull(startPackage); + assertNotNull(filename); + try { + final ParseResult<CompilationUnit> result = tryToParse(startPackage, filename); + if (result.isSuccessful()) { + return result.getResult().get(); + } + throw new ParseProblemException(result.getProblems()); + } catch (IOException e) { + throw new ParseProblemException(e); + } + } + + /** + * Tries to parse all .java files in a package recursively and passes them one by one to the callback. In comparison + * to the other parse methods, this is much more memory efficient, but saveAll() won't work. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + * @deprecated pass ParserConfiguration instead of JavaParser + */ + @Deprecated + public SourceRoot parse(String startPackage, JavaParser javaParser, Callback callback) throws IOException { + return parse(startPackage, javaParser.getParserConfiguration(), callback); + } + + /** + * Tries to parse all .java files in a package recursively and passes them one by one to the callback. In comparison + * to the other parse methods, this is much more memory efficient, but saveAll() won't work. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + */ + public SourceRoot parse(String startPackage, ParserConfiguration configuration, Callback callback) throws IOException { + assertNotNull(startPackage); + assertNotNull(configuration); + assertNotNull(callback); + logPackage(startPackage); + final JavaParser javaParser = new JavaParser(configuration); + final Path path = packageAbsolutePath(root, startPackage); + Files.walkFileTree(path, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path absolutePath, BasicFileAttributes attrs) throws IOException { + if (!attrs.isDirectory() && absolutePath.toString().endsWith(".java")) { + Path localPath = root.relativize(absolutePath); + Log.trace("Parsing %s", localPath); + final ParseResult<CompilationUnit> result = javaParser.parse(COMPILATION_UNIT, + provider(absolutePath)); + result.getResult().ifPresent(cu -> cu.setStorage(absolutePath)); + if (callback.process(localPath, absolutePath, result) == SAVE) { + if (result.getResult().isPresent()) { + save(result.getResult().get(), path); + } + } + } + return CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return isSensibleDirectoryToEnter(dir) ? CONTINUE : SKIP_SUBTREE; + } + }); + return this; + } + + private void logPackage(String startPackage) { + if (startPackage.isEmpty()) { + return; + } + Log.info("Parsing package \"%s\"", startPackage); + } + + /** + * Tries to parse all .java files in a package recursively using multiple threads, and passes them one by one to the + * callback. A new thread is forked each time a new directory is visited and is responsible for parsing all .java + * files in that directory. <b>Note that</b> the provided {@link Callback} code must be made thread-safe. <b>Note + * that</b> to ensure thread safety, a new parser instance is created for every file with the provided {@link + * ParserConfiguration}. In comparison to the other parse methods, this is much more memory efficient, but saveAll() + * won't work. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + */ + public SourceRoot parseParallelized(String startPackage, ParserConfiguration configuration, Callback callback) { + assertNotNull(startPackage); + assertNotNull(configuration); + assertNotNull(callback); + logPackage(startPackage); + final Path path = packageAbsolutePath(root, startPackage); + ParallelParse parse = new ParallelParse(path, (file, attrs) -> { + if (!attrs.isDirectory() && file.toString().endsWith(".java")) { + Path localPath = root.relativize(file); + Log.trace("Parsing %s", localPath); + try { + ParseResult<CompilationUnit> result = new JavaParser(configuration) + .parse(COMPILATION_UNIT, provider(file)); + result.getResult().ifPresent(cu -> cu.setStorage(file)); + if (callback.process(localPath, file, result) == SAVE) { + if (result.getResult().isPresent()) { + save(result.getResult().get(), path); + } + } + } catch (IOException e) { + Log.error(e); + } + } + return CONTINUE; + }); + ForkJoinPool pool = new ForkJoinPool(); + pool.invoke(parse); + return this; + } + + /** + * Tries to parse all .java files in a package recursively using multiple threads, and passes them one by one to the + * callback. A new thread is forked each time a new directory is visited and is responsible for parsing all .java + * files in that directory. <b>Note that</b> the provided {@link Callback} code must be made thread-safe. <b>Note + * that</b> to ensure thread safety, a new parser instance is created for every file. In comparison to the other + * parse methods, this is much more memory efficient, but saveAll() won't work. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + */ + public SourceRoot parseParallelized(String startPackage, Callback callback) throws IOException { + return parseParallelized(startPackage, new ParserConfiguration(), callback); + } + + /** + * Tries to parse all .java files recursively using multiple threads, and passes them one by one to the callback. A + * new thread is forked each time a new directory is visited and is responsible for parsing all .java files in that + * directory. <b>Note that</b> the provided {@link Callback} code must be made thread-safe. <b>Note that</b> to + * ensure thread safety, a new parser instance is created for every file. In comparison to the other parse methods, + * this is much more memory efficient, but saveAll() won't work. + */ + public SourceRoot parseParallelized(Callback callback) throws IOException { + return parseParallelized("", new ParserConfiguration(), callback); + } + + /** + * Add a newly created Java file to the cache of this source root. It will be saved when saveAll is called. + * + * @param startPackage files in this package and deeper are parsed. Pass "" to parse all files. + */ + public SourceRoot add(String startPackage, String filename, CompilationUnit compilationUnit) { + assertNotNull(startPackage); + assertNotNull(filename); + assertNotNull(compilationUnit); + Log.trace("Adding new file %s.%s", startPackage, filename); + final Path path = fileInPackageRelativePath(startPackage, filename); + final ParseResult<CompilationUnit> parseResult = new ParseResult<>( + compilationUnit, + new ArrayList<>(), + null, + null); + cache.put(path, parseResult); + return this; + } + + /** + * Add a newly created Java file to the cache of this source root. It will be saved when saveAll is called. It needs + * to have its path set. + */ + public SourceRoot add(CompilationUnit compilationUnit) { + assertNotNull(compilationUnit); + if (compilationUnit.getStorage().isPresent()) { + final Path path = compilationUnit.getStorage().get().getPath(); + Log.trace("Adding new file %s", path); + final ParseResult<CompilationUnit> parseResult = new ParseResult<>( + compilationUnit, + new ArrayList<>(), + null, + null); + cache.put(path, parseResult); + } else { + throw new AssertionError("Files added with this method should have their path set."); + } + return this; + } + + /** + * Save the given compilation unit to the given path. + */ + private SourceRoot save(CompilationUnit cu, Path path) { + assertNotNull(cu); + assertNotNull(path); + cu.setStorage(path); + cu.getStorage().get().save(printer); + return this; + } + + /** + * Save all previously parsed files back to a new path. + */ + public SourceRoot saveAll(Path root) { + assertNotNull(root); + Log.info("Saving all files (%s) to %s", cache.size(), root); + for (Map.Entry<Path, ParseResult<CompilationUnit>> cu : cache.entrySet()) { + final Path path = root.resolve(cu.getKey()); + if (cu.getValue().getResult().isPresent()) { + Log.trace("Saving %s", path); + save(cu.getValue().getResult().get(), path); + } + } + return this; + } + + /** + * Save all previously parsed files back to where they were found. + */ + public SourceRoot saveAll() { + return saveAll(root); + } + + /** + * The Java files that have been parsed by this source root object, or have been added manually. + */ + public List<ParseResult<CompilationUnit>> getCache() { + return new ArrayList<>(cache.values()); + } + + /** + * The CompilationUnits of the Java files that have been parsed succesfully by this source root object, or have been + * added manually. + */ + public List<CompilationUnit> getCompilationUnits() { + return cache.values().stream() + .filter(ParseResult::isSuccessful) + .map(p -> p.getResult().get()) + .collect(Collectors.toList()); + } + + /** + * The path that was passed in the constructor. + */ + public Path getRoot() { + return root; + } + + /** + * @deprecated store ParserConfiguration now + */ + @Deprecated + public JavaParser getJavaParser() { + return new JavaParser(parserConfiguration); + } + + /** + * Set the parser that is used for parsing by default. + * + * @deprecated store ParserConfiguration now + */ + @Deprecated + public SourceRoot setJavaParser(JavaParser javaParser) { + assertNotNull(javaParser); + this.parserConfiguration = javaParser.getParserConfiguration(); + return this; + } + + public ParserConfiguration getParserConfiguration() { + return parserConfiguration; + } + + /** + * Set the parser configuration that is used for parsing when no configuration is passed to a method. + */ + public SourceRoot setParserConfiguration(ParserConfiguration parserConfiguration) { + assertNotNull(parserConfiguration); + this.parserConfiguration = parserConfiguration; + return this; + } + + /** + * Set the printing function that transforms compilation units into a string to save. + */ + public SourceRoot setPrinter(Function<CompilationUnit, String> printer) { + assertNotNull(printer); + this.printer = printer; + return this; + } + + /** + * Get the printing function. + */ + public Function<CompilationUnit, String> getPrinter() { + return printer; + } + + /** + * Executes a recursive file tree walk using threads. A new thread is invoked for each new directory discovered + * during the walk. For each file visited, the user-provided {@link VisitFileCallback} is called with the current + * path and file attributes. Any shared resources accessed in a {@link VisitFileCallback} should be made + * thread-safe. + */ + private static class ParallelParse extends RecursiveAction { + + private static final long serialVersionUID = 1L; + private final Path path; + private final VisitFileCallback callback; + + ParallelParse(Path path, VisitFileCallback callback) { + this.path = path; + this.callback = callback; + } + + @Override + protected void compute() { + final List<ParallelParse> walks = new ArrayList<>(); + try { + Files.walkFileTree(path, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (!SourceRoot.isSensibleDirectoryToEnter(dir)) { + return SKIP_SUBTREE; + } + if (!dir.equals(ParallelParse.this.path)) { + ParallelParse w = new ParallelParse(dir, callback); + w.fork(); + walks.add(w); + return SKIP_SUBTREE; + } else { + return CONTINUE; + } + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + return callback.process(file, attrs); + } + }); + } catch (IOException e) { + Log.error(e); + } + + for (ParallelParse w : walks) { + w.join(); + } + } + + interface VisitFileCallback { + FileVisitResult process(Path file, BasicFileAttributes attrs); + } + } +} |