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