1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php 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 package com.android.ide.eclipse.adt.internal.build; 18 19 import com.android.ide.eclipse.adt.AdtConstants; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.AndroidConstants; 22 import com.android.ide.eclipse.adt.internal.build.BaseBuilder.BaseDeltaVisitor; 23 import com.android.ide.eclipse.adt.internal.build.PreCompilerBuilder.AidlData; 24 import com.android.ide.eclipse.adt.internal.project.AndroidManifestParser; 25 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 26 import com.android.sdklib.SdkConstants; 27 28 import org.eclipse.core.resources.IContainer; 29 import org.eclipse.core.resources.IFile; 30 import org.eclipse.core.resources.IFolder; 31 import org.eclipse.core.resources.IResource; 32 import org.eclipse.core.resources.IResourceDelta; 33 import org.eclipse.core.resources.IResourceDeltaVisitor; 34 import org.eclipse.core.resources.IWorkspaceRoot; 35 import org.eclipse.core.resources.ResourcesPlugin; 36 import org.eclipse.core.runtime.CoreException; 37 import org.eclipse.core.runtime.IPath; 38 39 import java.util.ArrayList; 40 41 /** 42 * Resource Delta visitor for the pre-compiler. 43 * <p/>This delta visitor only cares about files that are the source or the result of actions of the 44 * {@link PreCompilerBuilder}: 45 * <ul><li>R.java/Manifest.java generated by compiling the resources</li> 46 * <li>Any Java files generated by <code>aidl</code></li></ul>. 47 * 48 * Therefore it looks for the following: 49 * <ul><li>Any modification in the resource folder</li> 50 * <li>Removed files from the source folder receiving generated Java files</li> 51 * <li>Any modification to aidl files.</li> 52 * 53 */ 54 class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements 55 IResourceDeltaVisitor { 56 57 private enum AidlType { 58 UNKNOWN, INTERFACE, PARCELABLE; 59 } 60 61 // See comment in #getAidlType() 62 // private final static Pattern sParcelablePattern = Pattern.compile( 63 // "^\\s*parcelable\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*;\\s*$"); 64 // 65 // private final static Pattern sInterfacePattern = Pattern.compile( 66 // "^\\s*interface\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?:\\{.*)?$"); 67 68 // Result fields. 69 /** 70 * Compile flag. This is set to true if one of the changed/added/removed 71 * file is a resource file. Upon visiting all the delta resources, if 72 * this flag is true, then we know we'll have to compile the resources 73 * into R.java 74 */ 75 private boolean mCompileResources = false; 76 77 /** 78 * Aidl force recompilation flag. If true, we'll attempt to recompile all aidl files. 79 */ 80 private boolean mForceAidlCompile = false; 81 82 /** List of .aidl files found that are modified or new. */ 83 private final ArrayList<AidlData> mAidlToCompile = new ArrayList<AidlData>(); 84 85 /** List of .aidl files that have been removed. */ 86 private final ArrayList<AidlData> mAidlToRemove = new ArrayList<AidlData>(); 87 88 /** Manifest check/parsing flag. */ 89 private boolean mCheckedManifestXml = false; 90 91 /** Application Package, gathered from the parsing of the manifest */ 92 private String mJavaPackage = null; 93 /** minSDKVersion attribute value, gathered from the parsing of the manifest */ 94 private String mMinSdkVersion = null; 95 96 // Internal usage fields. 97 /** 98 * In Resource folder flag. This allows us to know if we're in the 99 * resource folder. 100 */ 101 private boolean mInRes = false; 102 103 /** 104 * Current Source folder. This allows us to know if we're in a source 105 * folder, and which folder. 106 */ 107 private IFolder mSourceFolder = null; 108 109 /** List of source folders. */ 110 private ArrayList<IPath> mSourceFolders; 111 private boolean mIsGenSourceFolder = false; 112 113 private IWorkspaceRoot mRoot; 114 115 PreCompilerDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders)116 public PreCompilerDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders) { 117 super(builder); 118 mSourceFolders = sourceFolders; 119 mRoot = ResourcesPlugin.getWorkspace().getRoot(); 120 } 121 getCompileResources()122 public boolean getCompileResources() { 123 return mCompileResources; 124 } 125 getForceAidlCompile()126 public boolean getForceAidlCompile() { 127 return mForceAidlCompile; 128 } 129 getAidlToCompile()130 public ArrayList<AidlData> getAidlToCompile() { 131 return mAidlToCompile; 132 } 133 getAidlToRemove()134 public ArrayList<AidlData> getAidlToRemove() { 135 return mAidlToRemove; 136 } 137 138 /** 139 * Returns whether the manifest file was parsed/checked for error during the resource delta 140 * visiting. 141 */ getCheckedManifestXml()142 public boolean getCheckedManifestXml() { 143 return mCheckedManifestXml; 144 } 145 146 /** 147 * Returns the manifest package if the manifest was checked/parsed. 148 * <p/> 149 * This can return null in two cases: 150 * <ul> 151 * <li>The manifest was not part of the resource change delta, and the manifest was 152 * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li> 153 * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>), 154 * but the package declaration is missing</li> 155 * </ul> 156 * @return the manifest package or null. 157 */ getManifestPackage()158 public String getManifestPackage() { 159 return mJavaPackage; 160 } 161 162 /** 163 * Returns the minSDkVersion attribute from the manifest if it was checked/parsed. 164 * <p/> 165 * This can return null in two cases: 166 * <ul> 167 * <li>The manifest was not part of the resource change delta, and the manifest was 168 * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li> 169 * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>), 170 * but the package declaration is missing</li> 171 * </ul> 172 * @return the minSdkVersion or null. 173 */ getMinSdkVersion()174 public String getMinSdkVersion() { 175 return mMinSdkVersion; 176 } 177 178 /* 179 * (non-Javadoc) 180 * 181 * @see org.eclipse.core.resources.IResourceDeltaVisitor 182 * #visit(org.eclipse.core.resources.IResourceDelta) 183 */ visit(IResourceDelta delta)184 public boolean visit(IResourceDelta delta) throws CoreException { 185 // we are only going to look for changes in res/, source folders and in 186 // AndroidManifest.xml since the delta visitor goes through the main 187 // folder before its children we can check when the path segment 188 // count is 2 (format will be /$Project/folder) and make sure we are 189 // processing res/, source folders or AndroidManifest.xml 190 191 IResource resource = delta.getResource(); 192 IPath path = resource.getFullPath(); 193 String[] segments = path.segments(); 194 195 // since the delta visitor also visits the root we return true if 196 // segments.length = 1 197 if (segments.length == 1) { 198 // FIXME: check this is an Android project. 199 return true; 200 } else if (segments.length == 2) { 201 // if we are at an item directly under the root directory, 202 // then we are not yet in a source or resource folder 203 mInRes = false; 204 mSourceFolder = null; 205 206 if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) { 207 // this is the resource folder that was modified. we want to 208 // see its content. 209 210 // since we're going to visit its children next, we set the 211 // flag 212 mInRes = true; 213 mSourceFolder = null; 214 return true; 215 } else if (AndroidConstants.FN_ANDROID_MANIFEST.equalsIgnoreCase(segments[1])) { 216 // any change in the manifest could trigger a new R.java 217 // class, so we don't need to check the delta kind 218 if (delta.getKind() != IResourceDelta.REMOVED) { 219 // parse the manifest for errors 220 AndroidManifestParser parser = BaseProjectHelper.parseManifestForError( 221 (IFile)resource, this); 222 223 if (parser != null) { 224 mJavaPackage = parser.getPackage(); 225 mMinSdkVersion = parser.getApiLevelRequirement(); 226 } 227 228 mCheckedManifestXml = true; 229 } 230 mCompileResources = true; 231 232 // we don't want to go to the children, not like they are 233 // any for this resource anyway. 234 return false; 235 } 236 } 237 238 // at this point we can either be in the source folder or in the 239 // resource folder or in a different folder that contains a source 240 // folder. 241 // This is due to not all source folder being src/. Some could be 242 // something/somethingelse/src/ 243 244 // so first we test if we already know we are in a source or 245 // resource folder. 246 247 if (mSourceFolder != null) { 248 // if we are in the res folder, we are looking for the following changes: 249 // - added/removed/modified aidl files. 250 // - missing R.java file 251 252 // if the resource is a folder, we just go straight to the children 253 if (resource.getType() == IResource.FOLDER) { 254 return true; 255 } 256 257 if (resource.getType() != IResource.FILE) { 258 return false; 259 } 260 IFile file = (IFile)resource; 261 262 // get the modification kind 263 int kind = delta.getKind(); 264 265 // we process normal source folder and the 'gen' source folder differently. 266 if (mIsGenSourceFolder) { 267 // this is the generated java file source folder. 268 // - if R.java/Manifest.java are removed/modified, we recompile the resources 269 // - if aidl files are removed/modified, we recompile them. 270 271 boolean outputWarning = false; 272 273 String fileName = resource.getName(); 274 275 // Special case of R.java/Manifest.java. 276 if (AndroidConstants.FN_RESOURCE_CLASS.equals(fileName) || 277 AndroidConstants.FN_MANIFEST_CLASS.equals(fileName)) { 278 // if it was removed, there's a possibility that it was removed due to a 279 // package change, or an aidl that was removed, but the only thing 280 // that will happen is that we'll have an extra build. Not much of a problem. 281 mCompileResources = true; 282 283 // we want a warning 284 outputWarning = true; 285 } else { 286 // this has to be a Java file created from an aidl file. 287 // Look for the source aidl file in all the source folders. 288 String aidlFileName = fileName.replaceAll(AndroidConstants.RE_JAVA_EXT, 289 AndroidConstants.DOT_AIDL); 290 291 for (IPath sourceFolderPath : mSourceFolders) { 292 // do not search in the current source folder as it is the 'gen' folder. 293 if (sourceFolderPath.equals(mSourceFolder.getFullPath())) { 294 continue; 295 } 296 297 IFolder sourceFolder = getFolder(sourceFolderPath); 298 if (sourceFolder != null) { 299 // go recursively, segment by segment. 300 // index starts at 2 (0 is project, 1 is 'gen' 301 IFile sourceFile = findFile(sourceFolder, segments, 2, aidlFileName); 302 303 if (sourceFile != null) { 304 // found the source. add it to the list of files to compile 305 mAidlToCompile.add(new AidlData(sourceFolder, sourceFile)); 306 outputWarning = true; 307 break; 308 } 309 } 310 } 311 } 312 313 if (outputWarning) { 314 if (kind == IResourceDelta.REMOVED) { 315 // We pring an error just so that it's red, but it's just a warning really. 316 String msg = String.format(Messages.s_Removed_Recreating_s, fileName); 317 AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); 318 } else if (kind == IResourceDelta.CHANGED) { 319 // the file was modified manually! we can't allow it. 320 String msg = String.format(Messages.s_Modified_Manually_Recreating_s, 321 fileName); 322 AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); 323 } 324 } 325 } else { 326 // this is another source folder. 327 // We only care about aidl files being added/modified/removed. 328 329 // get the extension of the resource 330 String ext = resource.getFileExtension(); 331 if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) { 332 // first check whether it's a regular file or a parcelable. 333 AidlType type = getAidlType(file); 334 335 if (type == AidlType.INTERFACE) { 336 if (kind == IResourceDelta.REMOVED) { 337 // we'll have to remove the generated file. 338 mAidlToRemove.add(new AidlData(mSourceFolder, file)); 339 } else if (mForceAidlCompile == false) { 340 // add the aidl file to the list of file to (re)compile 341 mAidlToCompile.add(new AidlData(mSourceFolder, file)); 342 } 343 } else { 344 // force recompilations of all Aidl Files. 345 mForceAidlCompile = true; 346 mAidlToCompile.clear(); 347 } 348 } 349 } 350 351 // no children. 352 return false; 353 } else if (mInRes) { 354 // if we are in the res folder, we are looking for the following 355 // changes: 356 // - added/removed/modified xml files. 357 // - added/removed files of any other type 358 359 // if the resource is a folder, we just go straight to the 360 // children 361 if (resource.getType() == IResource.FOLDER) { 362 return true; 363 } 364 365 // get the extension of the resource 366 String ext = resource.getFileExtension(); 367 int kind = delta.getKind(); 368 369 String p = resource.getProjectRelativePath().toString(); 370 String message = null; 371 switch (kind) { 372 case IResourceDelta.CHANGED: 373 // display verbose message 374 message = String.format(Messages.s_Modified_Recreating_s, p, 375 AndroidConstants.FN_RESOURCE_CLASS); 376 break; 377 case IResourceDelta.ADDED: 378 // display verbose message 379 message = String.format(Messages.Added_s_s_Needs_Updating, p, 380 AndroidConstants.FN_RESOURCE_CLASS); 381 break; 382 case IResourceDelta.REMOVED: 383 // display verbose message 384 message = String.format(Messages.s_Removed_s_Needs_Updating, p, 385 AndroidConstants.FN_RESOURCE_CLASS); 386 break; 387 } 388 if (message != null) { 389 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, 390 mBuilder.getProject(), message); 391 } 392 393 if (AndroidConstants.EXT_XML.equalsIgnoreCase(ext)) { 394 if (kind != IResourceDelta.REMOVED) { 395 // check xml Validity 396 mBuilder.checkXML(resource, this); 397 } 398 399 // if we are going through this resource, it was modified 400 // somehow. 401 // we don't care if it was an added/removed/changed event 402 mCompileResources = true; 403 return false; 404 } else { 405 // this is a non xml resource. 406 if (kind == IResourceDelta.ADDED 407 || kind == IResourceDelta.REMOVED) { 408 mCompileResources = true; 409 return false; 410 } 411 } 412 } else if (resource instanceof IFolder) { 413 // in this case we may be inside a folder that contains a source 414 // folder, go through the list of known source folders 415 416 for (IPath sourceFolderPath : mSourceFolders) { 417 // first check if they match exactly. 418 if (sourceFolderPath.equals(path)) { 419 // this is a source folder! 420 mInRes = false; 421 mSourceFolder = getFolder(sourceFolderPath); // all non null due to test above 422 mIsGenSourceFolder = path.segmentCount() == 2 && 423 path.segment(1).equals(SdkConstants.FD_GEN_SOURCES); 424 return true; 425 } 426 427 // check if we are on the way to a source folder. 428 int count = sourceFolderPath.matchingFirstSegments(path); 429 if (count == path.segmentCount()) { 430 mInRes = false; 431 return true; 432 } 433 } 434 435 // if we're here, we are visiting another folder 436 // like /$Project/bin/ for instance (we get notified for changes 437 // in .class!) 438 // This could also be another source folder and we have found 439 // R.java in a previous source folder 440 // We don't want to visit its children 441 return false; 442 } 443 444 return false; 445 } 446 447 /** 448 * Searches for and return a file in a folder. The file is defined by its segments, and a new 449 * name (replacing the last segment). 450 * @param folder the folder we are searching 451 * @param segments the segments of the file to search. 452 * @param index the index of the current segment we are looking for 453 * @param filename the new name to replace the last segment. 454 * @return the {@link IFile} representing the searched file, or null if not found 455 */ findFile(IFolder folder, String[] segments, int index, String filename)456 private IFile findFile(IFolder folder, String[] segments, int index, String filename) { 457 boolean lastSegment = index == segments.length - 1; 458 IResource resource = folder.findMember(lastSegment ? filename : segments[index]); 459 if (resource != null && resource.exists()) { 460 if (lastSegment) { 461 if (resource.getType() == IResource.FILE) { 462 return (IFile)resource; 463 } 464 } else { 465 if (resource.getType() == IResource.FOLDER) { 466 return findFile((IFolder)resource, segments, index+1, filename); 467 } 468 } 469 } 470 return null; 471 } 472 473 /** 474 * Returns a handle to the folder identified by the given path in this container. 475 * <p/>The different with {@link IContainer#getFolder(IPath)} is that this returns a non 476 * null object only if the resource actually exists and is a folder (and not a file) 477 * @param path the path of the folder to return. 478 * @return a handle to the folder if it exists, or null otherwise. 479 */ getFolder(IPath path)480 private IFolder getFolder(IPath path) { 481 IResource resource = mRoot.findMember(path); 482 if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) { 483 return (IFolder)resource; 484 } 485 486 return null; 487 } 488 489 /** 490 * Returns the type of the aidl file. Aidl files can either declare interfaces, or declare 491 * parcelables. This method will attempt to parse the file and return the type. If the type 492 * cannot be determined, then it will return {@link AidlType#UNKNOWN}. 493 * @param file The aidl file 494 * @return the type of the aidl. 495 * @throws CoreException 496 */ getAidlType(IFile file)497 private AidlType getAidlType(IFile file) throws CoreException { 498 // At this time, parsing isn't available, so we return UNKNOWN. This will force 499 // a recompilation of all aidl file as soon as one is changed. 500 return AidlType.UNKNOWN; 501 502 // TODO: properly parse aidl file to determine type and generate dependency graphs. 503 // 504 // String className = file.getName().substring(0, 505 // file.getName().length() - AndroidConstants.DOT_AIDL.length()); 506 // 507 // InputStream input = file.getContents(true /* force*/); 508 // try { 509 // BufferedReader reader = new BufferedReader(new InputStreamReader(input)); 510 // String line; 511 // while ((line = reader.readLine()) != null) { 512 // if (line.length() == 0) { 513 // continue; 514 // } 515 // 516 // Matcher m = sParcelablePattern.matcher(line); 517 // if (m.matches() && m.group(1).equals(className)) { 518 // return AidlType.PARCELABLE; 519 // } 520 // 521 // m = sInterfacePattern.matcher(line); 522 // if (m.matches() && m.group(1).equals(className)) { 523 // return AidlType.INTERFACE; 524 // } 525 // } 526 // } catch (IOException e) { 527 // throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 528 // "Error parsing aidl file", e)); 529 // } finally { 530 // try { 531 // input.close(); 532 // } catch (IOException e) { 533 // throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, 534 // "Error parsing aidl file", e)); 535 // } 536 // } 537 // 538 // return AidlType.UNKNOWN; 539 } 540 } 541