/* * 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.build; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.project.ProjectHelper; import com.android.ide.eclipse.adt.sdk.LoadStatus; import com.android.ide.eclipse.common.AndroidConstants; import com.android.ide.eclipse.common.project.BaseProjectHelper; import com.android.ide.eclipse.common.project.XmlErrorHandler; import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener; 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.IWorkspaceRoot; 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.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.xml.sax.SAXException; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; 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. */ abstract class BaseBuilder extends IncrementalProjectBuilder { // TODO: rename the pattern to something that makes sense + javadoc comments. /** * Single line aapt warning for skipping files.<br> * " (skipping hidden file '<file path>'" */ private final static Pattern sPattern0Line1 = Pattern.compile( "^\\s+\\(skipping hidden file\\s'(.*)'\\)$"); //$NON-NLS-1$ /** * First line of dual line aapt error.<br> * "ERROR at line <line>: <error>"<br> * " (Occurred while parsing <path>)" */ private final static Pattern sPattern1Line1 = Pattern.compile( "^ERROR\\s+at\\s+line\\s+(\\d+):\\s+(.*)$"); //$NON-NLS-1$ /** * Second line of dual line aapt error.<br> * "ERROR at line <line>: <error>"<br> * " (Occurred while parsing <path>)"<br> * @see #sPattern1Line1 */ private final static Pattern sPattern1Line2 = Pattern.compile( "^\\s+\\(Occurred while parsing\\s+(.*)\\)$"); //$NON-NLS-1$ /** * First line of dual line aapt error.<br> * "ERROR: <error>"<br> * "Defined at file <path> line <line>" */ private final static Pattern sPattern2Line1 = Pattern.compile( "^ERROR:\\s+(.+)$"); //$NON-NLS-1$ /** * Second line of dual line aapt error.<br> * "ERROR: <error>"<br> * "Defined at file <path> line <line>"<br> * @see #sPattern2Line1 */ private final static Pattern sPattern2Line2 = Pattern.compile( "Defined\\s+at\\s+file\\s+(.+)\\s+line\\s+(\\d+)"); //$NON-NLS-1$ /** * Single line aapt error<br> * "<path> line <line>: <error>" */ private final static Pattern sPattern3Line1 = Pattern.compile( "^(.+)\\sline\\s(\\d+):\\s(.+)$"); //$NON-NLS-1$ /** * First line of dual line aapt error.<br> * "ERROR parsing XML file <path>"<br> * "<error> at line <line>" */ private final static Pattern sPattern4Line1 = Pattern.compile( "^Error\\s+parsing\\s+XML\\s+file\\s(.+)$"); //$NON-NLS-1$ /** * Second line of dual line aapt error.<br> * "ERROR parsing XML file <path>"<br> * "<error> at line <line>"<br> * @see #sPattern4Line1 */ private final static Pattern sPattern4Line2 = Pattern.compile( "^(.+)\\s+at\\s+line\\s+(\\d+)$"); //$NON-NLS-1$ /** * Single line aapt warning<br> * "<path>:<line>: <error>" */ private final static Pattern sPattern5Line1 = Pattern.compile( "^(.+?):(\\d+):\\s+WARNING:(.+)$"); //$NON-NLS-1$ /** * Single line aapt error<br> * "<path>:<line>: <error>" */ private final static Pattern sPattern6Line1 = Pattern.compile( "^(.+?):(\\d+):\\s+(.+)$"); //$NON-NLS-1$ /** * 4 line aapt error<br> * "ERROR: 9-path image <path> malformed"<br> * Line 2 and 3 are taken as-is while line 4 is ignored (it repeats with<br> * 'ERROR: failure processing <path>) */ private final static Pattern sPattern7Line1 = Pattern.compile( "^ERROR:\\s+9-patch\\s+image\\s+(.+)\\s+malformed\\.$"); //$NON-NLS-1$ private final static Pattern sPattern8Line1 = Pattern.compile( "^(invalid resource directory name): (.*)$"); //$NON-NLS-1$ /** * 2 line aapt error<br> * "ERROR: Invalid configuration: foo"<br> * " ^^^"<br> * There's no need to parse the 2nd line. */ private final static Pattern sPattern9Line1 = Pattern.compile( "^Invalid configuration: (.+)$"); //$NON-NLS-1$ /** SAX Parser factory. */ private SAXParserFactory mParserFactory; /** * 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 checkds 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 */ public void errorFound() { mXmlError = true; } } 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 removeMarkersFromFile(file, AndroidConstants.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. * * @param markerId The id of the marker to add. * @param message the message associated with the mark * @param severity the severity of the marker. */ protected final void markProject(String markerId, String message, int severity) { BaseProjectHelper.addMarker(getProject(), markerId, message, severity); } /** * Removes markers from a file. * @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. */ protected final void removeMarkersFromFile(IFile file, String markerId) { try { if (file.exists()) { file.deleteMarkers(markerId, true, IResource.DEPTH_ZERO); } } catch (CoreException ce) { String msg = String.format(Messages.Marker_Delete_Error, markerId, file.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); } } /** * Removes markers from a project and its children. * @param project The project 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 static void removeMarkersFromProject(IProject project, String markerId) { try { if (project.exists()) { project.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE); } } catch (CoreException ce) { String msg = String.format(Messages.Marker_Delete_Error, markerId, project.getName()); AdtPlugin.printErrorToConsole(project, 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 results The array to store the stderr output * @return the process return code. * @throws InterruptedException */ protected final int grabProcessOutput(final Process process, final ArrayList<String> results) throws InterruptedException { // Due to the limited buffer size on windows for the standard io (stderr, stdout), we // *need* to read both stdout and stderr all the time. If we don't and a process output // a large amount, this could deadlock the process. // read the lines as they come. if null is returned, it's // because the process finished new Thread("") { //$NON-NLS-1$ @Override public void run() { // create a buffer to read the stderr output InputStreamReader is = new InputStreamReader(process.getErrorStream()); BufferedReader errReader = new BufferedReader(is); try { while (true) { String line = errReader.readLine(); if (line != null) { results.add(line); } else { break; } } } catch (IOException e) { // do nothing. } } }.start(); new Thread("") { //$NON-NLS-1$ @Override public void run() { InputStreamReader is = new InputStreamReader(process.getInputStream()); BufferedReader outReader = new BufferedReader(is); IProject project = getProject(); try { while (true) { String line = outReader.readLine(); if (line != null) { AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, line); } else { break; } } } catch (IOException e) { // do nothing. } } }.start(); // get the return code from the process return process.waitFor(); } /** * Parse the output of aapt and mark the incorrect file with error markers * * @param results the output of aapt * @param project the project containing the file to mark * @return true if the parsing failed, false if success. */ protected final boolean parseAaptOutput(ArrayList<String> results, IProject project) { // nothing to parse? just return false; if (results.size() == 0) { return false; } // get the root of the project so that we can make IFile from full // file path String osRoot = project.getLocation().toOSString(); Matcher m; for (int i = 0; i < results.size(); i++) { String p = results.get(i); m = sPattern0Line1.matcher(p); if (m.matches()) { // we ignore those (as this is an ignore message from aapt) continue; } m = sPattern1Line1.matcher(p); if (m.matches()) { String lineStr = m.group(1); String msg = m.group(2); // get the matcher for the next line. m = getNextLineMatcher(results, ++i, sPattern1Line2); if (m == null) { return true; } String location = m.group(1); // check the values and attempt to mark the file. if (checkAndMark(location, lineStr, msg, osRoot, project, AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } continue; } // this needs to be tested before Pattern2 since they both start with 'ERROR:' m = sPattern7Line1.matcher(p); if (m.matches()) { String location = m.group(1); String msg = p; // default msg is the line in case we don't find anything else if (++i < results.size()) { msg = results.get(i).trim(); if (++i < results.size()) { msg = msg + " - " + results.get(i).trim(); //$NON-NLS-1$ // skip the next line i++; } } // display the error if (checkAndMark(location, null, msg, osRoot, project, AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } // success, go to the next line continue; } m = sPattern2Line1.matcher(p); if (m.matches()) { // get the msg String msg = m.group(1); // get the matcher for the next line. m = getNextLineMatcher(results, ++i, sPattern2Line2); if (m == null) { return true; } String location = m.group(1); String lineStr = m.group(2); // check the values and attempt to mark the file. if (checkAndMark(location, lineStr, msg, osRoot, project, AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } continue; } m = sPattern3Line1.matcher(p); if (m.matches()) { String location = m.group(1); String lineStr = m.group(2); String msg = m.group(3); // check the values and attempt to mark the file. if (checkAndMark(location, lineStr, msg, osRoot, project, AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } // success, go to the next line continue; } m = sPattern4Line1.matcher(p); if (m.matches()) { // get the filename. String location = m.group(1); // get the matcher for the next line. m = getNextLineMatcher(results, ++i, sPattern4Line2); if (m == null) { return true; } String msg = m.group(1); String lineStr = m.group(2); // check the values and attempt to mark the file. if (checkAndMark(location, lineStr, msg, osRoot, project, AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } // success, go to the next line continue; } m = sPattern5Line1.matcher(p); if (m.matches()) { String location = m.group(1); String lineStr = m.group(2); String msg = m.group(3); // check the values and attempt to mark the file. if (checkAndMark(location, lineStr, msg, osRoot, project, AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { return true; } // success, go to the next line continue; } m = sPattern6Line1.matcher(p); if (m.matches()) { String location = m.group(1); String lineStr = m.group(2); String msg = m.group(3); // check the values and attempt to mark the file. if (checkAndMark(location, lineStr, msg, osRoot, project, AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } // success, go to the next line continue; } m = sPattern8Line1.matcher(p); if (m.matches()) { String location = m.group(2); String msg = m.group(1); // check the values and attempt to mark the file. if (checkAndMark(location, null, msg, osRoot, project, AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { return true; } // success, go to the next line continue; } m = sPattern9Line1.matcher(p); if (m.matches()) { String badConfig = m.group(1); String msg = String.format("APK Configuration filter '%1$s' is invalid", badConfig); // skip the next line i++; // check the values and attempt to mark the file. if (checkAndMark(null /*location*/, null, msg, osRoot, project, AndroidConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) { return true; } // success, go to the next line continue; } // invalid line format, flag as error, and bail return true; } return false; } /** * 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); } /** * Saves the path of a resource into the persistent storate of the project. * @param propertyName the name of the property. The id of the plugin is added to this string. * @param resource the resource which path is saved. * @return true if the save succeeded */ protected boolean saveProjectResourceProperty(String propertyName, IResource resource) { return ProjectHelper.saveResourceProperty(getProject(), propertyName, resource); } /** * Loads the path of a resource from the persistent storage of the project, and returns the * corresponding IResource object. * @param propertyName the name of the property. The id of the plugin is added to this string. * @return The corresponding IResource object (or children interface) or null */ protected IResource loadProjectResourceProperty(String propertyName) { IProject project = getProject(); return ProjectHelper.loadResourceProperty(project, propertyName); } /** * Check if the parameters gotten from the error output are valid, and mark * the file with an AAPT marker. * @param location the full OS path of the error file. If null, the project is marked * @param lineStr * @param message * @param root The root directory of the project, in OS specific format. * @param project * @param markerId The marker id to put. * @param severity The severity of the marker to put (IMarker.SEVERITY_*) * @return true if the parameters were valid and the file was marked successfully. * * @see IMarker */ private final boolean checkAndMark(String location, String lineStr, String message, String root, IProject project, String markerId, int severity) { // check this is in fact a file if (location != null) { File f = new File(location); if (f.exists() == false) { return false; } } // get the line number int line = -1; // default value for error with no line. if (lineStr != null) { try { line = Integer.parseInt(lineStr); } catch (NumberFormatException e) { // looks like the string we extracted wasn't a valid // file number. Parsing failed and we return true return false; } } // add the marker IResource f2 = project; if (location != null) { f2 = getResourceFromFullPath(location, root, project); if (f2 == null) { return false; } } // check if there's a similar marker already, since aapt is launched twice boolean markerAlreadyExists = false; try { IMarker[] markers = f2.findMarkers(markerId, true, IResource.DEPTH_ZERO); for (IMarker marker : markers) { int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); if (tmpLine != line) { break; } int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1); if (tmpSeverity != severity) { break; } String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null); if (tmpMsg == null || tmpMsg.equals(message) == false) { break; } // if we're here, all the marker attributes are equals, we found it // and exit markerAlreadyExists = true; break; } } catch (CoreException e) { // if we couldn't get the markers, then we just mark the file again // (since markerAlreadyExists is initialized to false, we do nothing) } if (markerAlreadyExists == false) { if (line != -1) { BaseProjectHelper.addMarker(f2, markerId, message, line, severity); } else { BaseProjectHelper.addMarker(f2, markerId, message, severity); } } return true; } /** * Returns a matching matcher for the next line * @param lines The array of lines * @param nextIndex The index of the next line * @param pattern The pattern to match * @return null if error or no match, the matcher otherwise. */ private final Matcher getNextLineMatcher(ArrayList<String> lines, int nextIndex, Pattern pattern) { // unless we can't, because we reached the last line if (nextIndex == lines.size()) { // we expected a 2nd line, so we flag as error // and we bail return null; } Matcher m = pattern.matcher(lines.get(nextIndex)); if (m.matches()) { return m; } return null; } private IResource getResourceFromFullPath(String filename, String root, IProject project) { if (filename.startsWith(root)) { String file = filename.substring(root.length()); // get the resource IResource r = project.findMember(file); // if the resource is valid, we add the marker if (r.exists()) { return r; } } return null; } /** * Returns an array of external jar files used by the project. * @return an array of OS-specific absolute file paths */ protected final String[] getExternalJars() { // get the current project IProject project = getProject(); // get a java project from it IJavaProject javaProject = JavaCore.create(project); IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); ArrayList<String> oslibraryList = new ArrayList<String>(); IClasspathEntry[] classpaths = javaProject.readRawClasspath(); if (classpaths != null) { for (IClasspathEntry e : classpaths) { if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY || e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { // if this is a classpath variable reference, we resolve it. if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { e = JavaCore.getResolvedClasspathEntry(e); } // get the IPath IPath path = e.getPath(); // check the name ends with .jar if (AndroidConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { boolean local = false; IResource resource = wsRoot.findMember(path); if (resource != null && resource.exists() && resource.getType() == IResource.FILE) { local = true; oslibraryList.add(resource.getLocation().toOSString()); } if (local == false) { // if the jar path doesn't match a workspace resource, // then we get an OSString and check if this links to a valid file. String osFullPath = path.toOSString(); File f = new File(osFullPath); if (f.exists()) { oslibraryList.add(osFullPath); } else { String message = String.format( Messages.Couldnt_Locate_s_Error, path); AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, message); // Also put a warning marker on the project markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_WARNING); } } } } } } return oslibraryList.toArray(new String[oslibraryList.size()]); } /** * Aborts the build if the SDK/project setups are broken. This does not * display any errors. * * @param project The {@link IJavaProject} being compiled. * @throws CoreException */ protected final void abortOnBadSetup(IProject project) throws CoreException { // check if we have finished loading the SDK. if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADED) { // we exit silently stopBuild("SDK is not loaded yet"); } // abort if there are TARGET or ADT type markers IMarker[] markers = project.findMarkers(AdtConstants.MARKER_TARGET, false /*includeSubtypes*/, IResource.DEPTH_ZERO); if (markers.length > 0) { stopBuild(""); } markers = project.findMarkers(AdtConstants.MARKER_ADT, false /*includeSubtypes*/, IResource.DEPTH_ZERO); if (markers.length > 0) { stopBuild(""); } } /** * Throws an exception to cancel the build. * * @param error the error message * @param args the printf-style arguments to the error message. * @throws CoreException */ protected final void stopBuild(String error, Object... args) throws CoreException { throw new CoreException(new Status(IStatus.CANCEL, AdtPlugin.PLUGIN_ID, String.format(error, args))); } /** * Recursively delete all the derived resources. */ protected void removeDerivedResources(IResource resource, IProgressMonitor monitor) throws CoreException { if (resource.exists()) { if (resource.isDerived()) { resource.delete(true, new SubProgressMonitor(monitor, 10)); } else if (resource.getType() == IResource.FOLDER) { IFolder folder = (IFolder)resource; IResource[] members = folder.members(); for (IResource member : members) { removeDerivedResources(member, monitor); } } } } }