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