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