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.builders; 18 19 import com.android.SdkConstants; 20 import com.android.ide.common.xml.ManifestData; 21 import com.android.ide.eclipse.adt.AdtConstants; 22 import com.android.ide.eclipse.adt.AdtPlugin; 23 import com.android.ide.eclipse.adt.internal.build.Messages; 24 import com.android.ide.eclipse.adt.internal.build.SourceChangeHandler; 25 import com.android.ide.eclipse.adt.internal.build.SourceProcessor; 26 import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder.BaseDeltaVisitor; 27 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; 28 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; 29 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 30 import com.android.ide.eclipse.adt.io.IFileWrapper; 31 32 import org.eclipse.core.resources.IContainer; 33 import org.eclipse.core.resources.IFile; 34 import org.eclipse.core.resources.IFolder; 35 import org.eclipse.core.resources.IResource; 36 import org.eclipse.core.resources.IResourceDelta; 37 import org.eclipse.core.resources.IResourceDeltaVisitor; 38 import org.eclipse.core.resources.IWorkspaceRoot; 39 import org.eclipse.core.resources.ResourcesPlugin; 40 import org.eclipse.core.runtime.CoreException; 41 import org.eclipse.core.runtime.IPath; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 46 /** 47 * Resource Delta visitor for the pre-compiler. 48 * <p/>This delta visitor only cares about files that are the source or the result of actions of the 49 * {@link PreCompilerBuilder}: 50 * <ul><li>R.java/Manifest.java generated by compiling the resources</li> 51 * <li>Any Java files generated by <code>aidl</code></li></ul>. 52 * 53 * Therefore it looks for the following: 54 * <ul><li>Any modification in the resource folder</li> 55 * <li>Removed files from the source folder receiving generated Java files</li> 56 * <li>Any modification to aidl files.</li> 57 * 58 */ 59 class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements IResourceDeltaVisitor { 60 61 // Result fields. 62 private boolean mChangedManifest = false; 63 64 /** 65 * Compile flag. This is set to true if one of the changed/added/removed 66 * files is Manifest.java, or R.java. All other file changes 67 * will be taken care of by ResourceManager. 68 */ 69 private boolean mCompileResources = false; 70 71 /** Manifest check/parsing flag. */ 72 private boolean mCheckedManifestXml = false; 73 74 /** Application Package, gathered from the parsing of the manifest */ 75 private String mJavaPackage = null; 76 /** minSDKVersion attribute value, gathered from the parsing of the manifest */ 77 private String mMinSdkVersion = null; 78 79 // Internal usage fields. 80 /** 81 * In Resource folder flag. This allows us to know if we're in the 82 * resource folder. 83 */ 84 private boolean mInRes = false; 85 86 /** 87 * Current Source folder. This allows us to know if we're in a source 88 * folder, and which folder. 89 */ 90 private IFolder mSourceFolder = null; 91 92 /** List of source folders. */ 93 private final List<IPath> mSourceFolders; 94 private boolean mIsGenSourceFolder = false; 95 96 private final List<SourceChangeHandler> mSourceChangeHandlers = 97 new ArrayList<SourceChangeHandler>(); 98 private final IWorkspaceRoot mRoot; 99 100 private IFolder mAndroidOutputFolder; 101 PreCompilerDeltaVisitor(BaseBuilder builder, List<IPath> sourceFolders, List<SourceProcessor> processors)102 public PreCompilerDeltaVisitor(BaseBuilder builder, List<IPath> sourceFolders, 103 List<SourceProcessor> processors) { 104 super(builder); 105 mSourceFolders = sourceFolders; 106 mRoot = ResourcesPlugin.getWorkspace().getRoot(); 107 108 for (SourceProcessor processor : processors) { 109 SourceChangeHandler handler = processor.getChangeHandler(); 110 mSourceChangeHandlers.add(handler); 111 } 112 113 mAndroidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(builder.getProject()); 114 } 115 116 /** 117 * Get whether Manifest.java, Manifest.xml, or R.java have changed 118 * @return true if any of Manifest.xml, Manifest.java, or R.java have been modified 119 */ getCompileResources()120 public boolean getCompileResources() { 121 return mCompileResources || mChangedManifest; 122 } 123 hasManifestChanged()124 public boolean hasManifestChanged() { 125 return mChangedManifest; 126 } 127 128 /** 129 * Returns whether the manifest file was parsed/checked for error during the resource delta 130 * visiting. 131 */ getCheckedManifestXml()132 public boolean getCheckedManifestXml() { 133 return mCheckedManifestXml; 134 } 135 136 /** 137 * Returns the manifest package if the manifest was checked/parsed. 138 * <p/> 139 * This can return null in two cases: 140 * <ul> 141 * <li>The manifest was not part of the resource change delta, and the manifest was 142 * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li> 143 * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>), 144 * but the package declaration is missing</li> 145 * </ul> 146 * @return the manifest package or null. 147 */ getManifestPackage()148 public String getManifestPackage() { 149 return mJavaPackage; 150 } 151 152 /** 153 * Returns the minSDkVersion attribute from the manifest if it was checked/parsed. 154 * <p/> 155 * This can return null in two cases: 156 * <ul> 157 * <li>The manifest was not part of the resource change delta, and the manifest was 158 * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li> 159 * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>), 160 * but the package declaration is missing</li> 161 * </ul> 162 * @return the minSdkVersion or null. 163 */ getMinSdkVersion()164 public String getMinSdkVersion() { 165 return mMinSdkVersion; 166 } 167 168 /* 169 * (non-Javadoc) 170 * 171 * @see org.eclipse.core.resources.IResourceDeltaVisitor 172 * #visit(org.eclipse.core.resources.IResourceDelta) 173 */ 174 @Override visit(IResourceDelta delta)175 public boolean visit(IResourceDelta delta) throws CoreException { 176 // we are only going to look for changes in res/, source folders and in 177 // AndroidManifest.xml since the delta visitor goes through the main 178 // folder before its children we can check when the path segment 179 // count is 2 (format will be /$Project/folder) and make sure we are 180 // processing res/, source folders or AndroidManifest.xml 181 182 IResource resource = delta.getResource(); 183 IPath path = resource.getFullPath(); 184 String[] segments = path.segments(); 185 186 // since the delta visitor also visits the root we return true if 187 // segments.length = 1 188 if (segments.length == 1) { 189 // this is always the Android project since we call 190 // Builder#getDelta(IProject) on the project itself. 191 return true; 192 } else if (segments.length == 2) { 193 // if we are at an item directly under the root directory, 194 // then we are not yet in a source or resource folder 195 mInRes = false; 196 mSourceFolder = null; 197 198 if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) { 199 // this is the resource folder that was modified. we want to 200 // see its content. 201 202 // since we're going to visit its children next, we set the 203 // flag 204 mInRes = true; 205 mSourceFolder = null; 206 return true; 207 } else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equalsIgnoreCase(segments[1])) { 208 // any change in the manifest could trigger a new R.java 209 // class, so we don't need to check the delta kind 210 if (delta.getKind() != IResourceDelta.REMOVED) { 211 // clean the error markers on the file. 212 IFile manifestFile = (IFile)resource; 213 214 if (manifestFile.exists()) { 215 manifestFile.deleteMarkers(AdtConstants.MARKER_XML, true, 216 IResource.DEPTH_ZERO); 217 manifestFile.deleteMarkers(AdtConstants.MARKER_ANDROID, true, 218 IResource.DEPTH_ZERO); 219 } 220 221 // parse the manifest for data and error 222 ManifestData manifestData = AndroidManifestHelper.parse( 223 new IFileWrapper(manifestFile), true /*gatherData*/, this); 224 225 if (manifestData != null) { 226 mJavaPackage = manifestData.getPackage(); 227 mMinSdkVersion = manifestData.getMinSdkVersionString(); 228 } 229 230 mCheckedManifestXml = true; 231 } 232 mChangedManifest = true; 233 234 // we don't want to go to the children, not like they are 235 // any for this resource anyway. 236 return false; 237 } 238 } 239 240 // at this point we can either be in the source folder or in the 241 // resource folder or in a different folder that contains a source 242 // folder. 243 // This is due to not all source folder being src/. Some could be 244 // something/somethingelse/src/ 245 246 // so first we test if we already know we are in a source or 247 // resource folder. 248 249 if (mSourceFolder != null) { 250 // if we are in the res folder, we are looking for the following changes: 251 // - added/removed/modified aidl files. 252 // - missing R.java file 253 254 // if the resource is a folder, we just go straight to the children 255 if (resource.getType() == IResource.FOLDER) { 256 return true; 257 } 258 259 if (resource.getType() != IResource.FILE) { 260 return false; 261 } 262 IFile file = (IFile)resource; 263 264 // get the modification kind 265 int kind = delta.getKind(); 266 267 // we process normal source folder and the 'gen' source folder differently. 268 if (mIsGenSourceFolder) { 269 // this is the generated java file source folder. 270 // - if R.java/Manifest.java are removed/modified, we recompile the resources 271 // - if aidl files are removed/modified, we recompile them. 272 273 boolean outputWarning = false; 274 275 String fileName = resource.getName(); 276 277 // Special case of R.java/Manifest.java. 278 if (SdkConstants.FN_RESOURCE_CLASS.equals(fileName) || 279 SdkConstants.FN_MANIFEST_CLASS.equals(fileName)) { 280 // if it was removed, there's a possibility that it was removed due to a 281 // package change, or an aidl that was removed, but the only thing 282 // that will happen is that we'll have an extra build. Not much of a problem. 283 mCompileResources = true; 284 285 // we want a warning 286 outputWarning = true; 287 } else { 288 // look to see if this file was generated by a processor. 289 for (SourceChangeHandler handler : mSourceChangeHandlers) { 290 if (handler.handleGeneratedFile(file, kind)) { 291 outputWarning = true; 292 break; // there shouldn't be 2 processors that handle the same file. 293 } 294 } 295 } 296 297 if (outputWarning) { 298 if (kind == IResourceDelta.REMOVED) { 299 // We pring an error just so that it's red, but it's just a warning really. 300 String msg = String.format(Messages.s_Removed_Recreating_s, fileName); 301 AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); 302 } else if (kind == IResourceDelta.CHANGED) { 303 // the file was modified manually! we can't allow it. 304 String msg = String.format(Messages.s_Modified_Manually_Recreating_s, 305 fileName); 306 AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg); 307 } 308 } 309 } else { 310 // this is another source folder. 311 for (SourceChangeHandler handler : mSourceChangeHandlers) { 312 handler.handleSourceFile(file, kind); 313 } 314 } 315 316 // no children. 317 return false; 318 } else if (mInRes) { 319 // if we are in the res folder, we are looking for the following 320 // changes: 321 // - added/removed/modified xml files. 322 // - added/removed files of any other type 323 324 // if the resource is a folder, we just go straight to the 325 // children 326 if (resource.getType() == IResource.FOLDER) { 327 return true; 328 } 329 330 // get the extension of the resource 331 String ext = resource.getFileExtension(); 332 int kind = delta.getKind(); 333 334 String p = resource.getProjectRelativePath().toString(); 335 String message = null; 336 switch (kind) { 337 case IResourceDelta.CHANGED: 338 // display verbose message 339 message = String.format(Messages.s_Modified_Recreating_s, p); 340 break; 341 case IResourceDelta.ADDED: 342 // display verbose message 343 message = String.format(Messages.Added_s_s_Needs_Updating, p, 344 SdkConstants.FN_RESOURCE_CLASS); 345 break; 346 case IResourceDelta.REMOVED: 347 // display verbose message 348 message = String.format(Messages.s_Removed_s_Needs_Updating, p, 349 SdkConstants.FN_RESOURCE_CLASS); 350 break; 351 } 352 if (message != null) { 353 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, 354 mBuilder.getProject(), message); 355 } 356 357 for (SourceChangeHandler handler : mSourceChangeHandlers) { 358 handler.handleResourceFile((IFile)resource, kind); 359 } 360 // If it's an XML resource, check the syntax 361 if (SdkConstants.EXT_XML.equalsIgnoreCase(ext) && kind != IResourceDelta.REMOVED) { 362 // check xml Validity 363 mBuilder.checkXML(resource, this); 364 } 365 // Whether or not to generate R.java for a changed resource is taken care of by the 366 // Resource Manager. 367 } else if (resource instanceof IFolder) { 368 // first check if we are in the android output folder. 369 if (resource.equals(mAndroidOutputFolder)) { 370 // we want to visit the merged manifest. 371 return true; 372 } 373 374 // in this case we may be inside a folder that contains a source 375 // folder, go through the list of known source folders 376 377 for (IPath sourceFolderPath : mSourceFolders) { 378 // first check if they match exactly. 379 if (sourceFolderPath.equals(path)) { 380 // this is a source folder! 381 mInRes = false; 382 mSourceFolder = getFolder(sourceFolderPath); // all non null due to test above 383 mIsGenSourceFolder = path.segmentCount() == 2 && 384 path.segment(1).equals(SdkConstants.FD_GEN_SOURCES); 385 return true; 386 } 387 388 // check if we are on the way to a source folder. 389 int count = sourceFolderPath.matchingFirstSegments(path); 390 if (count == path.segmentCount()) { 391 mInRes = false; 392 return true; 393 } 394 } 395 396 // if we're here, we are visiting another folder 397 // like /$Project/bin/ for instance (we get notified for changes 398 // in .class!) 399 // This could also be another source folder and we have found 400 // R.java in a previous source folder 401 // We don't want to visit its children 402 return false; 403 } 404 405 return false; 406 } 407 408 /** 409 * Returns a handle to the folder identified by the given path in this container. 410 * <p/>The different with {@link IContainer#getFolder(IPath)} is that this returns a non 411 * null object only if the resource actually exists and is a folder (and not a file) 412 * @param path the path of the folder to return. 413 * @return a handle to the folder if it exists, or null otherwise. 414 */ getFolder(IPath path)415 private IFolder getFolder(IPath path) { 416 IResource resource = mRoot.findMember(path); 417 if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) { 418 return (IFolder)resource; 419 } 420 421 return null; 422 } 423 424 } 425