blob: 162591406dc85782ed277b33769f8cd37ee06f0d [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.build.builders;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.sdk.LoadStatus;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.build.BuildHelper;
import com.android.ide.eclipse.adt.internal.build.Messages;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler;
import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.XmlErrorListener;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.adt.io.IFileWrapper;
import com.android.io.IAbstractFile;
import com.android.io.StreamException;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.repository.FullRevision;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IJavaProject;
import org.xml.sax.SAXException;
import java.util.ArrayList;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
/**
* Base builder for XML files. This class allows for basic XML parsing with
* error checking and marking the files for errors/warnings.
*/
public abstract class BaseBuilder extends IncrementalProjectBuilder {
protected static final boolean DEBUG_LOG = "1".equals( //$NON-NLS-1$
System.getenv("ANDROID_BUILD_DEBUG")); //$NON-NLS-1$
/** SAX Parser factory. */
private SAXParserFactory mParserFactory;
/**
* The build tool to use to build. This is guaranteed to be non null after a call to
* {@link #abortOnBadSetup(IJavaProject, ProjectState)} since this will throw if it can't be
* queried.
*/
protected BuildToolInfo mBuildToolInfo;
/**
* Base Resource Delta Visitor to handle XML error
*/
protected static class BaseDeltaVisitor implements XmlErrorListener {
/** The Xml builder used to validate XML correctness. */
protected BaseBuilder mBuilder;
/**
* XML error flag. if true, we keep parsing the ResourceDelta but the
* compilation will not happen (we're putting markers)
*/
public boolean mXmlError = false;
public BaseDeltaVisitor(BaseBuilder builder) {
mBuilder = builder;
}
/**
* Finds a matching Source folder for the current path. This checks if the current path
* leads to, or is a source folder.
* @param sourceFolders The list of source folders
* @param pathSegments The segments of the current path
* @return The segments of the source folder, or null if no match was found
*/
protected static String[] findMatchingSourceFolder(ArrayList<IPath> sourceFolders,
String[] pathSegments) {
for (IPath p : sourceFolders) {
// check if we are inside one of those source class path
// get the segments
String[] srcSegments = p.segments();
// compare segments. We want the path of the resource
// we're visiting to be
boolean valid = true;
int segmentCount = pathSegments.length;
for (int i = 0 ; i < segmentCount; i++) {
String s1 = pathSegments[i];
String s2 = srcSegments[i];
if (s1.equalsIgnoreCase(s2) == false) {
valid = false;
break;
}
}
if (valid) {
// this folder, or one of this children is a source
// folder!
// we return its segments
return srcSegments;
}
}
return null;
}
/**
* Sent when an XML error is detected.
* @see XmlErrorListener
*/
@Override
public void errorFound() {
mXmlError = true;
}
}
protected static class AbortBuildException extends Exception {
private static final long serialVersionUID = 1L;
}
public BaseBuilder() {
super();
mParserFactory = SAXParserFactory.newInstance();
// FIXME when the compiled XML support for namespace is in, set this to true.
mParserFactory.setNamespaceAware(false);
}
/**
* Checks an Xml file for validity. Errors/warnings will be marked on the
* file
* @param resource the resource to check
* @param visitor a valid resource delta visitor
*/
protected final void checkXML(IResource resource, BaseDeltaVisitor visitor) {
// first make sure this is an xml file
if (resource instanceof IFile) {
IFile file = (IFile)resource;
// remove previous markers
removeMarkersFromResource(file, AdtConstants.MARKER_XML);
// create the error handler
XmlErrorHandler reporter = new XmlErrorHandler(file, visitor);
try {
// parse
getParser().parse(file.getContents(), reporter);
} catch (Exception e1) {
}
}
}
/**
* Returns the SAXParserFactory, instantiating it first if it's not already
* created.
* @return the SAXParserFactory object
* @throws ParserConfigurationException
* @throws SAXException
*/
protected final SAXParser getParser() throws ParserConfigurationException,
SAXException {
return mParserFactory.newSAXParser();
}
/**
* Adds a marker to the current project. This methods catches thrown {@link CoreException},
* and returns null instead.
*
* @param markerId The id of the marker to add.
* @param message the message associated with the mark
* @param severity the severity of the marker.
* @return the marker that was created (or null if failure)
* @see IMarker
*/
protected final IMarker markProject(String markerId, String message, int severity) {
return BaseProjectHelper.markResource(getProject(), markerId, message, severity);
}
/**
* Removes markers from a resource and only the resource (not its children).
* @param file The file from which to delete the markers.
* @param markerId The id of the markers to remove. If null, all marker of
* type <code>IMarker.PROBLEM</code> will be removed.
*/
public final void removeMarkersFromResource(IResource resource, String markerId) {
try {
if (resource.exists()) {
resource.deleteMarkers(markerId, true, IResource.DEPTH_ZERO);
}
} catch (CoreException ce) {
String msg = String.format(Messages.Marker_Delete_Error, markerId, resource.toString());
AdtPlugin.printErrorToConsole(getProject(), msg);
}
}
/**
* Removes markers from a container and its children.
* @param folder The container from which to delete the markers.
* @param markerId The id of the markers to remove. If null, all marker of
* type <code>IMarker.PROBLEM</code> will be removed.
*/
protected final void removeMarkersFromContainer(IContainer folder, String markerId) {
try {
if (folder.exists()) {
folder.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE);
}
} catch (CoreException ce) {
String msg = String.format(Messages.Marker_Delete_Error, markerId, folder.toString());
AdtPlugin.printErrorToConsole(getProject(), msg);
}
}
/**
* Get the stderr output of a process and return when the process is done.
* @param process The process to get the ouput from
* @param stdErr The array to store the stderr output
* @return the process return code.
* @throws InterruptedException
*/
protected final int grabProcessOutput(final Process process,
final ArrayList<String> stdErr) throws InterruptedException {
return BuildHelper.grabProcessOutput(getProject(), process, stdErr);
}
/**
* Saves a String property into the persistent storage of the project.
* @param propertyName the name of the property. The id of the plugin is added to this string.
* @param value the value to save
* @return true if the save succeeded.
*/
protected boolean saveProjectStringProperty(String propertyName, String value) {
IProject project = getProject();
return ProjectHelper.saveStringProperty(project, propertyName, value);
}
/**
* Loads a String property from the persistent storage of the project.
* @param propertyName the name of the property. The id of the plugin is added to this string.
* @return the property value or null if it was not found.
*/
protected String loadProjectStringProperty(String propertyName) {
IProject project = getProject();
return ProjectHelper.loadStringProperty(project, propertyName);
}
/**
* Saves a property into the persistent storage of the project.
* @param propertyName the name of the property. The id of the plugin is added to this string.
* @param value the value to save
* @return true if the save succeeded.
*/
protected boolean saveProjectBooleanProperty(String propertyName, boolean value) {
IProject project = getProject();
return ProjectHelper.saveStringProperty(project, propertyName, Boolean.toString(value));
}
/**
* Loads a boolean property from the persistent storage of the project.
* @param propertyName the name of the property. The id of the plugin is added to this string.
* @param defaultValue The default value to return if the property was not found.
* @return the property value or the default value if the property was not found.
*/
protected boolean loadProjectBooleanProperty(String propertyName, boolean defaultValue) {
IProject project = getProject();
return ProjectHelper.loadBooleanProperty(project, propertyName, defaultValue);
}
/**
* Aborts the build if the SDK/project setups are broken. This does not
* display any errors.
*
* @param javaProject The {@link IJavaProject} being compiled.
* @param projectState the project state, optional. will be queried if null.
* @throws CoreException
*/
protected void abortOnBadSetup(@NonNull IJavaProject javaProject,
@Nullable ProjectState projectState) throws AbortBuildException, CoreException {
IProject iProject = javaProject.getProject();
// check if we have finished loading the project target.
Sdk sdk = Sdk.getCurrent();
if (sdk == null) {
throw new AbortBuildException();
}
if (projectState == null) {
projectState = Sdk.getProjectState(javaProject.getProject());
}
// get the target for the project
IAndroidTarget target = projectState.getTarget();
if (target == null) {
throw new AbortBuildException();
}
// check on the target data.
if (sdk.checkAndLoadTargetData(target, javaProject) != LoadStatus.LOADED) {
throw new AbortBuildException();
}
mBuildToolInfo = projectState.getBuildToolInfo();
if (mBuildToolInfo == null) {
mBuildToolInfo = sdk.getLatestBuildTool();
if (mBuildToolInfo == null) {
AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject,
"No \"Build Tools\" package available; use SDK Manager to install one.");
throw new AbortBuildException();
} else {
AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject,
String.format("Using default Build Tools revision %s",
mBuildToolInfo.getRevision())
);
}
}
// abort if there are TARGET or ADT type markers
stopOnMarker(iProject, AdtConstants.MARKER_TARGET, IResource.DEPTH_ZERO,
false /*checkSeverity*/);
stopOnMarker(iProject, AdtConstants.MARKER_ADT, IResource.DEPTH_ZERO,
false /*checkSeverity*/);
}
protected void stopOnMarker(IProject project, String markerType, int depth,
boolean checkSeverity)
throws AbortBuildException {
try {
IMarker[] markers = project.findMarkers(markerType, false /*includeSubtypes*/, depth);
if (markers.length > 0) {
if (checkSeverity == false) {
throw new AbortBuildException();
} else {
for (IMarker marker : markers) {
int severity = marker.getAttribute(IMarker.SEVERITY, -1 /*defaultValue*/);
if (severity == IMarker.SEVERITY_ERROR) {
throw new AbortBuildException();
}
}
}
}
} catch (CoreException e) {
// don't stop, something's really screwed up and the build will break later with
// a better error message.
}
}
/**
* Handles a {@link StreamException} by logging the info and marking the project.
* This should generally be followed by exiting the build process.
*
* @param e the exception
*/
protected void handleStreamException(StreamException e) {
IAbstractFile file = e.getFile();
String msg;
IResource target = getProject();
if (file instanceof IFileWrapper) {
target = ((IFileWrapper) file).getIFile();
if (e.getError() == StreamException.Error.OUTOFSYNC) {
msg = "File is Out of sync";
} else {
msg = "Error reading file. Read log for details";
}
} else {
if (e.getError() == StreamException.Error.OUTOFSYNC) {
msg = String.format("Out of sync file: %s", file.getOsLocation());
} else {
msg = String.format("Error reading file %s. Read log for details",
file.getOsLocation());
}
}
AdtPlugin.logAndPrintError(e, getProject().getName(), msg);
BaseProjectHelper.markResource(target, AdtConstants.MARKER_ADT, msg,
IMarker.SEVERITY_ERROR);
}
/**
* Handles a generic {@link Throwable} by logging the info and marking the project.
* This should generally be followed by exiting the build process.
*
* @param t the {@link Throwable}.
* @param message the message to log and to associate with the marker.
*/
protected void handleException(Throwable t, String message) {
AdtPlugin.logAndPrintError(t, getProject().getName(), message);
markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR);
}
/**
* Recursively delete all the derived resources from a root resource. The root resource is not
* deleted.
* @param rootResource the root resource
* @param monitor a progress monitor.
* @throws CoreException
*
*/
protected void removeDerivedResources(IResource rootResource, IProgressMonitor monitor)
throws CoreException {
removeDerivedResources(rootResource, false, monitor);
}
/**
* delete a resource and its children. returns true if the root resource was deleted. All
* sub-folders *will* be deleted if they were emptied (not if they started empty).
* @param rootResource the root resource
* @param deleteRoot whether to delete the root folder.
* @param monitor a progress monitor.
* @throws CoreException
*/
private void removeDerivedResources(IResource rootResource, boolean deleteRoot,
IProgressMonitor monitor) throws CoreException {
if (rootResource.exists()) {
// if it's a folder, delete derived member.
if (rootResource.getType() == IResource.FOLDER) {
IFolder folder = (IFolder)rootResource;
IResource[] members = folder.members();
boolean wasNotEmpty = members.length > 0;
for (IResource member : members) {
removeDerivedResources(member, true /*deleteRoot*/, monitor);
}
// if the folder had content that is now all removed, delete the folder.
if (deleteRoot && wasNotEmpty && folder.members().length == 0) {
rootResource.getLocation().toFile().delete();
}
}
// if the root resource is derived, delete it.
if (rootResource.isDerived()) {
rootResource.getLocation().toFile().delete();
}
}
}
protected void launchJob(Job newJob) {
newJob.setPriority(Job.BUILD);
newJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
newJob.schedule();
}
}