1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 import java.io.File; 18 import java.io.IOException; 19 import java.io.BufferedReader; 20 import java.io.FileReader; 21 import java.nio.file.Files; 22 import java.nio.file.Path; 23 import java.util.ArrayList; 24 import java.util.Collection; 25 import java.util.Collections; 26 import java.util.List; 27 import java.util.Set; 28 import java.util.TreeSet; 29 import java.util.SortedSet; 30 import java.util.regex.Pattern; 31 32 /** 33 * Immutable representation of an IDE configuration. Assumes that the current 34 * directory is the project's root directory. 35 */ 36 public class Configuration { 37 38 /** Java source tree roots. */ 39 public final SortedSet<File> sourceRoots; 40 41 /** Found .jar files (that weren't excluded). */ 42 public final List<File> jarFiles; 43 44 /** Excluded directories which may or may not be under a source root. */ 45 public final SortedSet<File> excludedDirs; 46 47 /** The root directory for this tool. */ 48 public final File toolDirectory; 49 50 /** File name used for excluded path files. */ 51 private static final String EXCLUDED_PATHS = "excluded-paths"; 52 53 /** The vendor directory. */ 54 private static final String VENDOR_PATH = "./vendor/"; 55 56 /** 57 * Constructs a Configuration by traversing the directory tree, looking 58 * for .java and .jar files and identifying source roots. 59 */ Configuration()60 public Configuration() throws IOException { 61 this.toolDirectory = new File("development/tools/idegen"); 62 if (!toolDirectory.isDirectory()) { 63 // The wrapper script should have already verified this. 64 throw new AssertionError("Not in root directory."); 65 } 66 67 Stopwatch stopwatch = new Stopwatch(); 68 69 Excludes excludes = readExcludes(); 70 71 stopwatch.reset("Read excludes"); 72 73 List<File> jarFiles = new ArrayList<File>(500); 74 SortedSet<File> excludedDirs = new TreeSet<File>(); 75 SortedSet<File> sourceRoots = new TreeSet<File>(); 76 77 traverse(new File("."), sourceRoots, jarFiles, excludedDirs, excludes); 78 79 stopwatch.reset("Traversed tree"); 80 81 Log.debug(sourceRoots.size() + " source roots"); 82 Log.debug(jarFiles.size() + " jar files"); 83 Log.debug(excludedDirs.size() + " excluded dirs"); 84 85 this.sourceRoots = Collections.unmodifiableSortedSet(sourceRoots); 86 this.jarFiles = Collections.unmodifiableList(jarFiles); 87 this.excludedDirs = Collections.unmodifiableSortedSet(excludedDirs); 88 } 89 90 /** 91 * Reads excluded path files. 92 */ readExcludes()93 private Excludes readExcludes() throws IOException { 94 List<Pattern> patterns = new ArrayList<Pattern>(); 95 96 File globalExcludes = new File(toolDirectory, EXCLUDED_PATHS); 97 parseFile(globalExcludes, patterns); 98 99 // Traverse all vendor-specific directories 100 readVendorExcludes(patterns); 101 102 // Look for user-specific excluded-paths file in current directory. 103 File localExcludes = new File(EXCLUDED_PATHS); 104 if (localExcludes.exists()) { 105 parseFile(localExcludes, patterns); 106 } 107 108 return new Excludes(patterns); 109 } 110 111 /** 112 * Reads vendor excluded path files. 113 * @see #readExcludes() 114 */ readVendorExcludes(List<Pattern> out)115 private static void readVendorExcludes(List<Pattern> out) throws IOException { 116 File vendorDir = new File(VENDOR_PATH); 117 File[] vendorList; 118 if (!vendorDir.exists() || (vendorList = vendorDir.listFiles()) == null) return; 119 for (File vendor : vendorList) { 120 File vendorExcludes = new File(vendor, EXCLUDED_PATHS); 121 if (vendorExcludes.exists()) { 122 Log.info("Read vendor excludes: " + vendorExcludes.getPath()); 123 parseFile(vendorExcludes, out); 124 } 125 } 126 } 127 128 /** 129 * Recursively finds .java source roots, .jar files, and excluded 130 * directories. 131 */ traverse(File directory, Set<File> sourceRoots, Collection<File> jarFiles, Collection<File> excludedDirs, Excludes excludes)132 private static void traverse(File directory, Set<File> sourceRoots, 133 Collection<File> jarFiles, Collection<File> excludedDirs, 134 Excludes excludes) throws IOException { 135 /* 136 * Note it would be faster to stop traversing a source root as soon as 137 * we encounter the first .java file, but it appears we have nested 138 * source roots in our generated source directory (specifically, 139 * R.java files and aidl .java files don't share the same source 140 * root). 141 */ 142 143 boolean firstJavaFile = true; 144 File[] files = directory.listFiles(); 145 if (files == null) { 146 return; 147 } 148 for (File file : files) { 149 // Trim preceding "./" from path. 150 String path = file.getPath().substring(2); 151 152 // Skip nonexistent files/diretories, e.g. broken symlinks. 153 if (!file.exists()) { 154 Log.debug("Skipped nonexistent: " + path); 155 continue; 156 } 157 158 if (Files.isSymbolicLink(file.toPath())) { 159 Path target = Files.readSymbolicLink(file.toPath()).normalize(); 160 if (target.startsWith("") || target.startsWith(".") 161 || target.startsWith("..")) { 162 // Don't recurse symbolic link that targets to parent 163 // or current directory. 164 Log.debug("Skipped: " + path); 165 continue; 166 } 167 } 168 169 if (file.isDirectory()) { 170 // Traverse nested directories. 171 if (excludes.exclude(path)) { 172 // Don't recurse into excluded dirs. 173 Log.debug("Excluding: " + path); 174 excludedDirs.add(file); 175 } else { 176 traverse(file, sourceRoots, jarFiles, excludedDirs, 177 excludes); 178 } 179 } else if (path.endsWith(".java")) { 180 // Keep track of source roots for .java files. 181 // Do not check excludes in this branch. 182 if (firstJavaFile) { 183 // Only parse one .java file per directory. 184 firstJavaFile = false; 185 186 File sourceRoot = rootOf(file); 187 if (sourceRoot != null) { 188 sourceRoots.add(sourceRoot); 189 } 190 } 191 } else if (path.endsWith(".jar")) { 192 // Keep track of .jar files. 193 if (excludes.exclude(path)) { 194 Log.debug("Skipped: " + file); 195 } else { 196 jarFiles.add(file); 197 } 198 } 199 } 200 } 201 202 /** 203 * Determines the source root for a given .java file. Returns null 204 * if the file doesn't have a package or if the file isn't in the 205 * correct directory structure. 206 */ rootOf(File javaFile)207 private static File rootOf(File javaFile) throws IOException { 208 String packageName = parsePackageName(javaFile); 209 if (packageName == null) { 210 // No package. 211 // TODO: Treat this as a source root? 212 return null; 213 } 214 215 String packagePath = packageName.replace('.', File.separatorChar); 216 File parent = javaFile.getParentFile(); 217 String parentPath = parent.getPath(); 218 if (!parentPath.endsWith(packagePath)) { 219 // Bad dir structure. 220 return null; 221 } 222 223 return new File(parentPath.substring( 224 0, parentPath.length() - packagePath.length())); 225 } 226 227 /** 228 * Reads a Java file and parses out the package name. Returns null if none 229 * found. 230 */ parsePackageName(File file)231 private static String parsePackageName(File file) throws IOException { 232 try (BufferedReader in = new BufferedReader(new FileReader(file))) { 233 String line; 234 while ((line = in.readLine()) != null) { 235 String trimmed = line.trim(); 236 if (trimmed.startsWith("package")) { 237 // TODO: Make this more robust. 238 // Assumes there's only once space after "package" and the 239 // line ends in a ";". 240 return trimmed.substring(8, trimmed.length() - 1); 241 } 242 } 243 244 return null; 245 } 246 } 247 248 /** 249 * Picks out excluded directories that are under source roots. 250 */ excludesUnderSourceRoots()251 public SortedSet<File> excludesUnderSourceRoots() { 252 // TODO: Refactor this to share the similar logic in 253 // Eclipse.constructExcluding(). 254 SortedSet<File> picked = new TreeSet<File>(); 255 for (File sourceRoot : sourceRoots) { 256 String sourcePath = sourceRoot.getPath() + "/"; 257 SortedSet<File> tailSet = excludedDirs.tailSet(sourceRoot); 258 for (File file : tailSet) { 259 if (file.getPath().startsWith(sourcePath)) { 260 picked.add(file); 261 } else { 262 break; 263 } 264 } 265 } 266 return picked; 267 } 268 269 /** 270 * Reads a list of regular expressions from a file, one per line, and adds 271 * the compiled patterns to the given collection. Ignores lines starting 272 * with '#'. 273 * 274 * @param file containing regular expressions, one per line 275 * @param patterns collection to add compiled patterns from file to 276 */ parseFile(File file, Collection<Pattern> patterns)277 public static void parseFile(File file, Collection<Pattern> patterns) 278 throws IOException { 279 try (BufferedReader in = new BufferedReader(new FileReader(file))) { 280 String line; 281 while ((line = in.readLine()) != null) { 282 String trimmed = line.trim(); 283 if (trimmed.length() > 0 && !trimmed.startsWith("#")) { 284 patterns.add(Pattern.compile(trimmed)); 285 } 286 } 287 } 288 } 289 } 290