package com.twitter.common.tools; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map.Entry; import java.util.Set; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; /** * A file manager that intercepts requests for class output files to track dependencies. * * Stores dependencies from source file to class file in a line oriented plain text format where * each line has the following format: * <pre> * [source file path] -> [class file path] * </pre> * * There may be multiple lines per source file if the file contains multiple top level classes or * inner classes. All paths are normalized to be relative to the classfile output directory. */ final class DependencyTrackingFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> { private final LinkedHashMap<String, List<String>> sourceToClasses = new LinkedHashMap<String, List<String>>(); private final Set<String> priorSources = new HashSet<String>(); private final File dependencyFile; private List<String> outputPath; private File outputDir; DependencyTrackingFileManager(StandardJavaFileManager fileManager, File dependencies) throws IOException { super(fileManager); this.dependencyFile = dependencies; if (dependencyFile.exists()) { System.out.println("Reading existing dependency file at " + dependencies); BufferedReader dependencyReader = new BufferedReader(new FileReader(dependencies)); try { int line = 0; while (true) { String mapping = dependencyReader.readLine(); if (mapping == null) { break; } line++; String[] components = mapping.split(" -> "); if (components.length != 2) { System.err.printf("Ignoring malformed dependency in %s[%d]: %s\n", dependencies, line, mapping); } else { String sourceRelpath = components[0]; String classRelpath = components[1]; addMapping(sourceRelpath, classRelpath); } } } finally { dependencyReader.close(); } } priorSources.addAll(sourceToClasses.keySet()); } @Override public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) throws IOException { JavaFileObject file = super.getJavaFileForOutput(location, className, kind, sibling); if (Kind.CLASS == kind) { addMapping(toOutputRelpath(sibling), toOutputRelpath(file)); } return file; } private void addMapping(String sourceFile, String classFile) { List<String> classFiles = sourceToClasses.get(sourceFile); if (classFiles == null || priorSources.remove(sourceFile)) { classFiles = new ArrayList<String>(); sourceToClasses.put(sourceFile, classFiles); } classFiles.add(classFile); } private String toOutputRelpath(FileObject file) { List<String> base = new ArrayList<String>(getOutputPath()); List<String> path = toList(file); for (Iterator<String> baseIter = base.iterator(), pathIter = path.iterator(); baseIter.hasNext() && pathIter.hasNext();) { if (!baseIter.next().equals(pathIter.next())) { break; } else { baseIter.remove(); pathIter.remove(); } } if (!base.isEmpty()) { path.addAll(0, Collections.nCopies(base.size(), "..")); } return join(path); } private String join(List<String> components) { StringBuilder path = new StringBuilder(); for (int i = 0, max = components.size(); i < max; i++) { if (i > 0) { path.append(File.separatorChar); } path.append(components.get(i)); } return path.toString(); } private List<String> toList(FileObject path) { return new ArrayList<String>(Arrays.asList(path.toUri().normalize().getPath().split("/"))); } private synchronized List<String> getOutputPath() { if (outputPath == null) { List<String> components = new ArrayList<String>(); File f = getOutputDir(); while (f != null) { components.add(f.getName()); f = f.getParentFile(); } Collections.reverse(components); outputPath = components; } return outputPath; } private synchronized File getOutputDir() { if (outputDir == null) { Iterable<? extends File> location = fileManager.getLocation(StandardLocation.CLASS_OUTPUT); if (location == null || !location.iterator().hasNext()) { throw new IllegalStateException("Expected to be called after compilation started - found " + "no class output dir."); } for (File path : location) { if (outputDir != null) { throw new IllegalStateException("Expected exactly 1 output path"); } outputDir = path; } } return outputDir; } @Override public void close() throws IOException { super.close(); System.out.println("Writing class dependency file to " + dependencyFile); PrintWriter dependencyWriter = new PrintWriter(new FileWriter(dependencyFile, false)); try { for (Entry<String, List<String>> entry : sourceToClasses.entrySet()) { String sourceFile = entry.getKey(); for (String classFile : entry.getValue()) { if (!priorSources.contains(sourceFile) || doesMappingExist(sourceFile, classFile)) { dependencyWriter.printf("%s -> %s\n", sourceFile, classFile); } } } } finally { dependencyWriter.close(); } } private boolean doesMappingExist(String sourceFile, String classFile) { return new File(getOutputDir(), sourceFile).exists() && new File(getOutputDir(), classFile).exists(); } }