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.annotations.NonNull; 20 import com.android.annotations.Nullable; 21 import com.android.ide.common.sdk.LoadStatus; 22 import com.android.ide.eclipse.adt.AdtConstants; 23 import com.android.ide.eclipse.adt.AdtPlugin; 24 import com.android.ide.eclipse.adt.internal.build.BuildHelper; 25 import com.android.ide.eclipse.adt.internal.build.Messages; 26 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; 27 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 28 import com.android.ide.eclipse.adt.internal.project.ProjectHelper; 29 import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler; 30 import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.XmlErrorListener; 31 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 32 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 33 import com.android.ide.eclipse.adt.io.IFileWrapper; 34 import com.android.io.IAbstractFile; 35 import com.android.io.StreamException; 36 import com.android.sdklib.BuildToolInfo; 37 import com.android.sdklib.IAndroidTarget; 38 import com.android.sdklib.repository.FullRevision; 39 40 import org.eclipse.core.resources.IContainer; 41 import org.eclipse.core.resources.IFile; 42 import org.eclipse.core.resources.IFolder; 43 import org.eclipse.core.resources.IMarker; 44 import org.eclipse.core.resources.IProject; 45 import org.eclipse.core.resources.IResource; 46 import org.eclipse.core.resources.IncrementalProjectBuilder; 47 import org.eclipse.core.resources.ResourcesPlugin; 48 import org.eclipse.core.runtime.CoreException; 49 import org.eclipse.core.runtime.IPath; 50 import org.eclipse.core.runtime.IProgressMonitor; 51 import org.eclipse.core.runtime.jobs.Job; 52 import org.eclipse.jdt.core.IJavaProject; 53 import org.xml.sax.SAXException; 54 55 import java.util.ArrayList; 56 57 import javax.xml.parsers.ParserConfigurationException; 58 import javax.xml.parsers.SAXParser; 59 import javax.xml.parsers.SAXParserFactory; 60 61 /** 62 * Base builder for XML files. This class allows for basic XML parsing with 63 * error checking and marking the files for errors/warnings. 64 */ 65 public abstract class BaseBuilder extends IncrementalProjectBuilder { 66 67 protected static final boolean DEBUG_LOG = "1".equals( //$NON-NLS-1$ 68 System.getenv("ANDROID_BUILD_DEBUG")); //$NON-NLS-1$ 69 70 /** SAX Parser factory. */ 71 private SAXParserFactory mParserFactory; 72 73 /** 74 * The build tool to use to build. This is guaranteed to be non null after a call to 75 * {@link #abortOnBadSetup(IJavaProject, ProjectState)} since this will throw if it can't be 76 * queried. 77 */ 78 protected BuildToolInfo mBuildToolInfo; 79 80 /** 81 * Base Resource Delta Visitor to handle XML error 82 */ 83 protected static class BaseDeltaVisitor implements XmlErrorListener { 84 85 /** The Xml builder used to validate XML correctness. */ 86 protected BaseBuilder mBuilder; 87 88 /** 89 * XML error flag. if true, we keep parsing the ResourceDelta but the 90 * compilation will not happen (we're putting markers) 91 */ 92 public boolean mXmlError = false; 93 BaseDeltaVisitor(BaseBuilder builder)94 public BaseDeltaVisitor(BaseBuilder builder) { 95 mBuilder = builder; 96 } 97 98 /** 99 * Finds a matching Source folder for the current path. This checks if the current path 100 * leads to, or is a source folder. 101 * @param sourceFolders The list of source folders 102 * @param pathSegments The segments of the current path 103 * @return The segments of the source folder, or null if no match was found 104 */ findMatchingSourceFolder(ArrayList<IPath> sourceFolders, String[] pathSegments)105 protected static String[] findMatchingSourceFolder(ArrayList<IPath> sourceFolders, 106 String[] pathSegments) { 107 108 for (IPath p : sourceFolders) { 109 // check if we are inside one of those source class path 110 111 // get the segments 112 String[] srcSegments = p.segments(); 113 114 // compare segments. We want the path of the resource 115 // we're visiting to be 116 boolean valid = true; 117 int segmentCount = pathSegments.length; 118 119 for (int i = 0 ; i < segmentCount; i++) { 120 String s1 = pathSegments[i]; 121 String s2 = srcSegments[i]; 122 123 if (s1.equalsIgnoreCase(s2) == false) { 124 valid = false; 125 break; 126 } 127 } 128 129 if (valid) { 130 // this folder, or one of this children is a source 131 // folder! 132 // we return its segments 133 return srcSegments; 134 } 135 } 136 137 return null; 138 } 139 140 /** 141 * Sent when an XML error is detected. 142 * @see XmlErrorListener 143 */ 144 @Override errorFound()145 public void errorFound() { 146 mXmlError = true; 147 } 148 } 149 150 protected static class AbortBuildException extends Exception { 151 private static final long serialVersionUID = 1L; 152 } 153 BaseBuilder()154 public BaseBuilder() { 155 super(); 156 mParserFactory = SAXParserFactory.newInstance(); 157 158 // FIXME when the compiled XML support for namespace is in, set this to true. 159 mParserFactory.setNamespaceAware(false); 160 } 161 162 /** 163 * Checks an Xml file for validity. Errors/warnings will be marked on the 164 * file 165 * @param resource the resource to check 166 * @param visitor a valid resource delta visitor 167 */ checkXML(IResource resource, BaseDeltaVisitor visitor)168 protected final void checkXML(IResource resource, BaseDeltaVisitor visitor) { 169 170 // first make sure this is an xml file 171 if (resource instanceof IFile) { 172 IFile file = (IFile)resource; 173 174 // remove previous markers 175 removeMarkersFromResource(file, AdtConstants.MARKER_XML); 176 177 // create the error handler 178 XmlErrorHandler reporter = new XmlErrorHandler(file, visitor); 179 try { 180 // parse 181 getParser().parse(file.getContents(), reporter); 182 } catch (Exception e1) { 183 } 184 } 185 } 186 187 /** 188 * Returns the SAXParserFactory, instantiating it first if it's not already 189 * created. 190 * @return the SAXParserFactory object 191 * @throws ParserConfigurationException 192 * @throws SAXException 193 */ getParser()194 protected final SAXParser getParser() throws ParserConfigurationException, 195 SAXException { 196 return mParserFactory.newSAXParser(); 197 } 198 199 /** 200 * Adds a marker to the current project. This methods catches thrown {@link CoreException}, 201 * and returns null instead. 202 * 203 * @param markerId The id of the marker to add. 204 * @param message the message associated with the mark 205 * @param severity the severity of the marker. 206 * @return the marker that was created (or null if failure) 207 * @see IMarker 208 */ markProject(String markerId, String message, int severity)209 protected final IMarker markProject(String markerId, String message, int severity) { 210 return BaseProjectHelper.markResource(getProject(), markerId, message, severity); 211 } 212 213 /** 214 * Removes markers from a resource and only the resource (not its children). 215 * @param file The file from which to delete the markers. 216 * @param markerId The id of the markers to remove. If null, all marker of 217 * type <code>IMarker.PROBLEM</code> will be removed. 218 */ removeMarkersFromResource(IResource resource, String markerId)219 public final void removeMarkersFromResource(IResource resource, String markerId) { 220 try { 221 if (resource.exists()) { 222 resource.deleteMarkers(markerId, true, IResource.DEPTH_ZERO); 223 } 224 } catch (CoreException ce) { 225 String msg = String.format(Messages.Marker_Delete_Error, markerId, resource.toString()); 226 AdtPlugin.printErrorToConsole(getProject(), msg); 227 } 228 } 229 230 /** 231 * Removes markers from a container and its children. 232 * @param folder The container from which to delete the markers. 233 * @param markerId The id of the markers to remove. If null, all marker of 234 * type <code>IMarker.PROBLEM</code> will be removed. 235 */ removeMarkersFromContainer(IContainer folder, String markerId)236 protected final void removeMarkersFromContainer(IContainer folder, String markerId) { 237 try { 238 if (folder.exists()) { 239 folder.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE); 240 } 241 } catch (CoreException ce) { 242 String msg = String.format(Messages.Marker_Delete_Error, markerId, folder.toString()); 243 AdtPlugin.printErrorToConsole(getProject(), msg); 244 } 245 } 246 247 /** 248 * Get the stderr output of a process and return when the process is done. 249 * @param process The process to get the ouput from 250 * @param stdErr The array to store the stderr output 251 * @return the process return code. 252 * @throws InterruptedException 253 */ grabProcessOutput(final Process process, final ArrayList<String> stdErr)254 protected final int grabProcessOutput(final Process process, 255 final ArrayList<String> stdErr) throws InterruptedException { 256 return BuildHelper.grabProcessOutput(getProject(), process, stdErr); 257 } 258 259 260 261 /** 262 * Saves a String property into the persistent storage of the project. 263 * @param propertyName the name of the property. The id of the plugin is added to this string. 264 * @param value the value to save 265 * @return true if the save succeeded. 266 */ saveProjectStringProperty(String propertyName, String value)267 protected boolean saveProjectStringProperty(String propertyName, String value) { 268 IProject project = getProject(); 269 return ProjectHelper.saveStringProperty(project, propertyName, value); 270 } 271 272 273 /** 274 * Loads a String property from the persistent storage of the project. 275 * @param propertyName the name of the property. The id of the plugin is added to this string. 276 * @return the property value or null if it was not found. 277 */ loadProjectStringProperty(String propertyName)278 protected String loadProjectStringProperty(String propertyName) { 279 IProject project = getProject(); 280 return ProjectHelper.loadStringProperty(project, propertyName); 281 } 282 283 /** 284 * Saves a property into the persistent storage of the project. 285 * @param propertyName the name of the property. The id of the plugin is added to this string. 286 * @param value the value to save 287 * @return true if the save succeeded. 288 */ saveProjectBooleanProperty(String propertyName, boolean value)289 protected boolean saveProjectBooleanProperty(String propertyName, boolean value) { 290 IProject project = getProject(); 291 return ProjectHelper.saveStringProperty(project, propertyName, Boolean.toString(value)); 292 } 293 294 /** 295 * Loads a boolean property from the persistent storage of the project. 296 * @param propertyName the name of the property. The id of the plugin is added to this string. 297 * @param defaultValue The default value to return if the property was not found. 298 * @return the property value or the default value if the property was not found. 299 */ loadProjectBooleanProperty(String propertyName, boolean defaultValue)300 protected boolean loadProjectBooleanProperty(String propertyName, boolean defaultValue) { 301 IProject project = getProject(); 302 return ProjectHelper.loadBooleanProperty(project, propertyName, defaultValue); 303 } 304 305 /** 306 * Aborts the build if the SDK/project setups are broken. This does not 307 * display any errors. 308 * 309 * @param javaProject The {@link IJavaProject} being compiled. 310 * @param projectState the project state, optional. will be queried if null. 311 * @throws CoreException 312 */ abortOnBadSetup(@onNull IJavaProject javaProject, @Nullable ProjectState projectState)313 protected void abortOnBadSetup(@NonNull IJavaProject javaProject, 314 @Nullable ProjectState projectState) throws AbortBuildException, CoreException { 315 IProject iProject = javaProject.getProject(); 316 // check if we have finished loading the project target. 317 Sdk sdk = Sdk.getCurrent(); 318 if (sdk == null) { 319 throw new AbortBuildException(); 320 } 321 322 if (projectState == null) { 323 projectState = Sdk.getProjectState(javaProject.getProject()); 324 } 325 326 // get the target for the project 327 IAndroidTarget target = projectState.getTarget(); 328 329 if (target == null) { 330 throw new AbortBuildException(); 331 } 332 333 // check on the target data. 334 if (sdk.checkAndLoadTargetData(target, javaProject) != LoadStatus.LOADED) { 335 throw new AbortBuildException(); 336 } 337 338 mBuildToolInfo = projectState.getBuildToolInfo(); 339 if (mBuildToolInfo == null) { 340 mBuildToolInfo = sdk.getLatestBuildTool(); 341 342 if (mBuildToolInfo == null) { 343 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject, 344 "No \"Build Tools\" package available; use SDK Manager to install one."); 345 throw new AbortBuildException(); 346 } else { 347 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject, 348 String.format("Using default Build Tools revision %s", 349 mBuildToolInfo.getRevision()) 350 ); 351 } 352 } 353 354 // abort if there are TARGET or ADT type markers 355 stopOnMarker(iProject, AdtConstants.MARKER_TARGET, IResource.DEPTH_ZERO, 356 false /*checkSeverity*/); 357 stopOnMarker(iProject, AdtConstants.MARKER_ADT, IResource.DEPTH_ZERO, 358 false /*checkSeverity*/); 359 } 360 stopOnMarker(IProject project, String markerType, int depth, boolean checkSeverity)361 protected void stopOnMarker(IProject project, String markerType, int depth, 362 boolean checkSeverity) 363 throws AbortBuildException { 364 try { 365 IMarker[] markers = project.findMarkers(markerType, false /*includeSubtypes*/, depth); 366 367 if (markers.length > 0) { 368 if (checkSeverity == false) { 369 throw new AbortBuildException(); 370 } else { 371 for (IMarker marker : markers) { 372 int severity = marker.getAttribute(IMarker.SEVERITY, -1 /*defaultValue*/); 373 if (severity == IMarker.SEVERITY_ERROR) { 374 throw new AbortBuildException(); 375 } 376 } 377 } 378 } 379 } catch (CoreException e) { 380 // don't stop, something's really screwed up and the build will break later with 381 // a better error message. 382 } 383 } 384 385 /** 386 * Handles a {@link StreamException} by logging the info and marking the project. 387 * This should generally be followed by exiting the build process. 388 * 389 * @param e the exception 390 */ handleStreamException(StreamException e)391 protected void handleStreamException(StreamException e) { 392 IAbstractFile file = e.getFile(); 393 394 String msg; 395 396 IResource target = getProject(); 397 if (file instanceof IFileWrapper) { 398 target = ((IFileWrapper) file).getIFile(); 399 400 if (e.getError() == StreamException.Error.OUTOFSYNC) { 401 msg = "File is Out of sync"; 402 } else { 403 msg = "Error reading file. Read log for details"; 404 } 405 406 } else { 407 if (e.getError() == StreamException.Error.OUTOFSYNC) { 408 msg = String.format("Out of sync file: %s", file.getOsLocation()); 409 } else { 410 msg = String.format("Error reading file %s. Read log for details", 411 file.getOsLocation()); 412 } 413 } 414 415 AdtPlugin.logAndPrintError(e, getProject().getName(), msg); 416 BaseProjectHelper.markResource(target, AdtConstants.MARKER_ADT, msg, 417 IMarker.SEVERITY_ERROR); 418 } 419 420 /** 421 * Handles a generic {@link Throwable} by logging the info and marking the project. 422 * This should generally be followed by exiting the build process. 423 * 424 * @param t the {@link Throwable}. 425 * @param message the message to log and to associate with the marker. 426 */ handleException(Throwable t, String message)427 protected void handleException(Throwable t, String message) { 428 AdtPlugin.logAndPrintError(t, getProject().getName(), message); 429 markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR); 430 } 431 432 /** 433 * Recursively delete all the derived resources from a root resource. The root resource is not 434 * deleted. 435 * @param rootResource the root resource 436 * @param monitor a progress monitor. 437 * @throws CoreException 438 * 439 */ removeDerivedResources(IResource rootResource, IProgressMonitor monitor)440 protected void removeDerivedResources(IResource rootResource, IProgressMonitor monitor) 441 throws CoreException { 442 removeDerivedResources(rootResource, false, monitor); 443 } 444 445 /** 446 * delete a resource and its children. returns true if the root resource was deleted. All 447 * sub-folders *will* be deleted if they were emptied (not if they started empty). 448 * @param rootResource the root resource 449 * @param deleteRoot whether to delete the root folder. 450 * @param monitor a progress monitor. 451 * @throws CoreException 452 */ removeDerivedResources(IResource rootResource, boolean deleteRoot, IProgressMonitor monitor)453 private void removeDerivedResources(IResource rootResource, boolean deleteRoot, 454 IProgressMonitor monitor) throws CoreException { 455 if (rootResource.exists()) { 456 // if it's a folder, delete derived member. 457 if (rootResource.getType() == IResource.FOLDER) { 458 IFolder folder = (IFolder)rootResource; 459 IResource[] members = folder.members(); 460 boolean wasNotEmpty = members.length > 0; 461 for (IResource member : members) { 462 removeDerivedResources(member, true /*deleteRoot*/, monitor); 463 } 464 465 // if the folder had content that is now all removed, delete the folder. 466 if (deleteRoot && wasNotEmpty && folder.members().length == 0) { 467 rootResource.getLocation().toFile().delete(); 468 } 469 } 470 471 // if the root resource is derived, delete it. 472 if (rootResource.isDerived()) { 473 rootResource.getLocation().toFile().delete(); 474 } 475 } 476 } 477 launchJob(Job newJob)478 protected void launchJob(Job newJob) { 479 newJob.setPriority(Job.BUILD); 480 newJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); 481 newJob.schedule(); 482 } 483 } 484