All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.javaparser.utils.SourceRoot Maven / Gradle / Ivy

/*
 * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
 * Copyright (C) 2011, 2013-2023 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */
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.DefaultPrettyPrinter;

import java.io.IOException;
import java.nio.charset.Charset;
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.*;
import static com.github.javaparser.utils.Utils.assertNotNull;
import static java.nio.file.FileVisitResult.*;

/**
 * A collection of Java source files located in one directory and its subdirectories on the file system. The root directory
 * corresponds to the root of the package structure of the source files within. Files can be parsed and written back one
 * by one or all together. Note that the internal cache used is thread-safe.
 * 
    *
  • methods called "tryToParse..." will return their result inside a "ParseResult", which supports parse successes and failures.
  • *
  • methods called "parse..." will return "CompilationUnit"s. If a file fails to parse, an exception is thrown.
  • *
  • methods ending in "...Parallelized" will speed up parsing by using multiple threads.
  • *
*/ public class SourceRoot { @FunctionalInterface public interface Callback { enum Result { SAVE, DONT_SAVE, TERMINATE } /** * @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 parsing the file. */ Result process(Path localPath, Path absolutePath, ParseResult result); } private final Path root; private final Map> cache = new ConcurrentHashMap<>(); private ParserConfiguration parserConfiguration = new ParserConfiguration(); private Function printer = new DefaultPrettyPrinter()::print; private static final Pattern JAVA_IDENTIFIER = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); /** * @param root the root directory of a set of source files. It corresponds to the root of the package structure of the * source files within, like "javaparser/javaparser-core/src/main/java" */ public SourceRoot(Path root) { assertNotNull(root); if (!Files.isDirectory(root)) { throw new IllegalArgumentException("Only directories are allowed as root path: " + root); } this.root = root.normalize(); Log.info("New source root at \"%s\"", () -> this.root); } /** * @param root the root directory of a set of source files. It corresponds to the root of the package structure of the * source files within, like "javaparser/javaparser-core/src/main/java" */ 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. */ public ParseResult 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 result = new JavaParser(configuration).parse(COMPILATION_UNIT, provider(path, configuration.getCharacterEncoding())); result.getResult().ifPresent(cu -> cu.setStorage(path, configuration.getCharacterEncoding())); 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 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> tryToParse(String startPackage) throws IOException { assertNotNull(startPackage); logPackage(startPackage); final Path path = packageAbsolutePath(root, startPackage); Files.walkFileTree(path, new SimpleFileVisitor() { @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(); } boolean isSensibleDirectoryToEnter(Path dir) throws IOException { final String dirToEnter = dir.getFileName().toString(); // Don't enter directories that cannot be packages. final boolean directoryIsAValidJavaIdentifier = JAVA_IDENTIFIER.matcher(dirToEnter).matches(); // Don't enter directories that are hidden, assuming that people don't store source files in hidden directories. // But we can enter in root directory even if the root directory is not considered as a valid java identifier if (!root.equals(dir) && (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> 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. Note that to ensure thread safety, a new parser instance is created for * every file with the internal parser's (i.e. {@link #setParserConfiguration(ParserConfiguration)}) 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> 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. Note that to ensure thread safety, a new parser instance is * created for every file with the internal (i.e. {@link #setParserConfiguration(ParserConfiguration)}) 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> tryToParseParallelized() { 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 result = tryToParse(startPackage, filename); if (result.isSuccessful()) { return result.getResult().get(); } throw new ParseProblemException(result.getProblems()); } catch (IOException e) { throw new ParseProblemException(e); } } private FileVisitResult callback(Path absolutePath, ParserConfiguration configuration, Callback callback) throws IOException { Path localPath = root.relativize(absolutePath); Log.trace("Parsing %s", () -> localPath); ParseResult result = new JavaParser(configuration).parse(COMPILATION_UNIT, provider(absolutePath, configuration.getCharacterEncoding())); result.getResult().ifPresent(cu -> cu.setStorage(absolutePath, configuration.getCharacterEncoding())); switch(callback.process(localPath, absolutePath, result)) { case SAVE: result.getResult().ifPresent(cu -> save(cu, absolutePath)); case DONT_SAVE: return CONTINUE; case TERMINATE: return TERMINATE; default: throw new AssertionError("Return an enum defined in SourceRoot.Callback.Result"); } } /** * Locates the .java file with the provided package and file name, parses it and passes it to the * callback. In comparison to the other parse methods, this is much more memory efficient, but saveAll() won't work. * * @param startPackage The package containing the file * @param filename The name of the file */ public SourceRoot parse(String startPackage, String filename, ParserConfiguration configuration, Callback callback) throws IOException { assertNotNull(startPackage); assertNotNull(filename); assertNotNull(configuration); assertNotNull(callback); callback(fileInPackageAbsolutePath(root, startPackage, filename), configuration, callback); return this; } /** * Parses the provided .java file and passes it to the callback. In comparison to the other parse methods, this * makes is much more memory efficient., but saveAll() won't work. */ public SourceRoot parse(String startPackage, String filename, Callback callback) throws IOException { parse(startPackage, filename, parserConfiguration, callback); return this; } /** * 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 Path path = packageAbsolutePath(root, startPackage); if (Files.exists(path)) { Files.walkFileTree(path, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path absolutePath, BasicFileAttributes attrs) throws IOException { if (!attrs.isDirectory() && absolutePath.toString().endsWith(".java")) { return callback(absolutePath, configuration, callback); } return CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { return isSensibleDirectoryToEnter(dir) ? CONTINUE : SKIP_SUBTREE; } }); } return this; } public SourceRoot parse(String startPackage, Callback callback) throws IOException { parse(startPackage, parserConfiguration, callback); 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. Note that the provided {@link Callback} code must be made thread-safe. Note * that 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); if (Files.exists(path)) { ParallelParse parse = new ParallelParse(path, (absolutePath, attrs) -> { if (!attrs.isDirectory() && absolutePath.toString().endsWith(".java")) { try { return callback(absolutePath, configuration, callback); } 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. Note that the provided {@link Callback} code must be made thread-safe. Note * that 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, this.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. Note that the provided {@link Callback} code must be made thread-safe. Note that 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("", this.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 parseResult = new ParseResult<>(compilationUnit, new ArrayList<>(), 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 parseResult = new ParseResult<>(compilationUnit, new ArrayList<>(), 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. * @param cu the compilation unit * @param path the path of the java file */ private SourceRoot save(CompilationUnit cu, Path path) { return save(cu, path, parserConfiguration.getCharacterEncoding()); } /** * Save the given compilation unit to the given path. * @param cu the compilation unit * @param path the path of the java file * @param encoding the encoding to use while saving the file */ private SourceRoot save(CompilationUnit cu, Path path, Charset encoding) { assertNotNull(cu); assertNotNull(path); cu.setStorage(path, encoding); cu.getStorage().get().save(printer); return this; } /** * Save all previously parsed files back to a new path. * @param root the root of the java packages * @param encoding the encoding to use while saving the file */ public SourceRoot saveAll(Path root, Charset encoding) { assertNotNull(root); Log.info("Saving all files (%s) to %s", cache::size, () -> root); for (Map.Entry> 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, encoding); } } return this; } /** * Save all previously parsed files back to a new path. * @param root the root of the java packages */ public SourceRoot saveAll(Path root) { return saveAll(root, parserConfiguration.getCharacterEncoding()); } /** * Save all previously parsed files back to where they were found. */ public SourceRoot saveAll() { return saveAll(root); } /** * Save all previously parsed files back to where they were found, with the given encoding. * @param encoding the encoding to use. */ public SourceRoot saveAll(Charset encoding) { return saveAll(root, encoding); } /** * The Java files that have been parsed by this source root object, or have been added manually. */ public List> 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 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; } 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 printer) { assertNotNull(printer); this.printer = printer; return this; } /** * Get the printing function. */ public Function 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 SourceRoot root; private final VisitFileCallback callback; ParallelParse(Path path, VisitFileCallback callback) { this.root = new SourceRoot(path); this.callback = callback; } @Override protected void compute() { final List walks = new ArrayList<>(); Path path = root.getRoot(); try { Files.walkFileTree(path, new SimpleFileVisitor() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (!root.isSensibleDirectoryToEnter(dir)) { return SKIP_SUBTREE; } if (!dir.equals(path)) { ParallelParse w = new ParallelParse(dir, callback); w.fork(); walks.add(w); return SKIP_SUBTREE; } 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); } } @Override public String toString() { return "SourceRoot at " + root; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy