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