diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java | 783 |
1 files changed, 783 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java new file mode 100644 index 000000000..1f17fb7c6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java @@ -0,0 +1,783 @@ +/* + * Copyright (C) 2010 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; + +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.text.FindReplaceDocumentAdapter; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.texteditor.IDocumentProvider; + +import java.io.File; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class AaptParser { + + // 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$ + + /** + * Portion of the error message which states the context in which the error occurred, + * such as which property was being processed and what the string value was that + * caused the error. + * <p> + * Example: + * error: No resource found that matches the given name (at 'text' with value '@string/foo') + */ + private static final Pattern sValueRangePattern = + Pattern.compile("\\(at '(.+)' with value '(.*)'\\)"); //$NON-NLS-1$ + + + /** + * Portion of error message which points to the second occurrence of a repeated resource + * definition. + * <p> + * Example: + * error: Resource entry repeatedStyle1 already has bag item android:gravity. + */ + private static final Pattern sRepeatedRangePattern = + Pattern.compile("Resource entry (.+) already has bag item (.+)\\."); //$NON-NLS-1$ + + /** + * Error message emitted when aapt skips a file because for example it's name is + * invalid, such as a layout file name which starts with _. + * <p> + * This error message is used by AAPT in Tools 19 and earlier. + */ + private static final Pattern sSkippingPattern = + Pattern.compile(" \\(skipping (.+) .+ '(.*)'\\)"); //$NON-NLS-1$ + + /** + * Error message emitted when aapt skips a file because for example it's name is + * invalid, such as a layout file name which starts with _. + * <p> + * This error message is used by AAPT in Tools 20 and later. + */ + private static final Pattern sNewSkippingPattern = + Pattern.compile(" \\(skipping .+ '(.+)' due to ANDROID_AAPT_IGNORE pattern '.+'\\)"); //$NON-NLS-1$ + + /** + * Suffix of error message which points to the first occurrence of a repeated resource + * definition. + * Example: + * Originally defined here. + */ + private static final String ORIGINALLY_DEFINED_MSG = "Originally defined here."; //$NON-NLS-1$ + + /** + * Portion of error message which points to the second occurrence of a repeated resource + * definition. + * <p> + * Example: + * error: Resource entry repeatedStyle1 already has bag item android:gravity. + */ + private static final Pattern sNoResourcePattern = + Pattern.compile("No resource found that matches the given name: attr '(.+)'\\."); //$NON-NLS-1$ + + /** + * Portion of error message which points to a missing required attribute in a + * resource definition. + * <p> + * Example: + * error: error: A 'name' attribute is required for <style> + */ + private static final Pattern sRequiredPattern = + Pattern.compile("A '(.+)' attribute is required for <(.+)>"); //$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$ + + private final static Pattern sXmlBlockPattern = Pattern.compile( + "W/ResourceType\\(.*\\): Bad XML block: no root element node found"); //$NON-NLS-1$ + + /** + * 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. + */ + public static boolean parseOutput(List<String> results, IProject project) { + int size = results.size(); + if (size > 0) { + return parseOutput(results.toArray(new String[size]), project); + } + + return false; + } + + /** + * 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. + */ + public static boolean parseOutput(String[] results, IProject project) { + // nothing to parse? just return false; + if (results.length == 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.length ; i++) { + String p = results[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, + AdtConstants.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.length) { + msg = results[i].trim(); + if (++i < results.length) { + msg = msg + " - " + results[i].trim(); //$NON-NLS-1$ + + // skip the next line + i++; + } + } + + // display the error + if (checkAndMark(location, null, msg, osRoot, project, + AdtConstants.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, + AdtConstants.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, + AdtConstants.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, + AdtConstants.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, + AdtConstants.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, + AdtConstants.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, + AdtConstants.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, + AdtConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sNewSkippingPattern.matcher(p); + if (m.matches()) { + String location = m.group(1); + + if (location.startsWith(".") //$NON-NLS-1$ + || location.endsWith("~")) { //$NON-NLS-1$ + continue; + } + + // check the values and attempt to mark the file. + if (checkAndMark(location, null, p.trim(), osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sSkippingPattern.matcher(p); + if (m.matches()) { + String location = m.group(2); + + // Certain files can safely be skipped without marking the project + // as having errors. See isHidden() in AaptAssets.cpp: + String type = m.group(1); + if (type.equals("backup") //$NON-NLS-1$ // main.xml~, etc + || type.equals("hidden") //$NON-NLS-1$ // .gitignore, etc + || type.equals("index")) { //$NON-NLS-1$ // thumbs.db, etc + continue; + } + + // check the values and attempt to mark the file. + if (checkAndMark(location, null, p.trim(), osRoot, project, + AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { + return true; + } + + // success, go to the next line + continue; + } + + m = sXmlBlockPattern.matcher(p); + if (m.matches()) { + // W/ResourceType(12345): Bad XML block: no root element node found + // Sadly there's NO filename reference; this error typically describes the + // error *after* this line. + if (results.length == 1) { + // This is the only error message: dump to console and quit + return true; + } + // Continue: the real culprit is displayed next and should get a marker + continue; + } + + return true; + } + + return false; + } + + /** + * 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 static 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; + } + } + + // Attempt to determine the exact range of characters affected by this error. + // This will look up the actual text of the file, go to the particular error line + // and scan for the specific string mentioned in the error. + int startOffset = -1; + int endOffset = -1; + if (f2 instanceof IFile) { + IRegion region = findRange((IFile) f2, line, message); + if (region != null) { + startOffset = region.getOffset(); + endOffset = startOffset + region.getLength(); + } + } + + // 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) { + if (startOffset != -1) { + int tmpBegin = marker.getAttribute(IMarker.CHAR_START, -1); + if (tmpBegin != startOffset) { + break; + } + int tmpEnd = marker.getAttribute(IMarker.CHAR_END, -1); + if (tmpEnd != startOffset) { + break; + } + } + + 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) { + BaseProjectHelper.markResource(f2, markerId, message, line, + startOffset, endOffset, severity); + } + + return true; + } + + /** + * Given an aapt error message in a given file and a given (initial) line number, + * return the corresponding offset range for the error, or null. + */ + private static IRegion findRange(IFile file, int line, String message) { + Matcher matcher = sValueRangePattern.matcher(message); + if (matcher.find()) { + String property = matcher.group(1); + String value = matcher.group(2); + + // First find the property. We can't just immediately look for the + // value, because there could be other attributes in this element + // earlier than the one in error, and we might accidentally pick + // up on a different occurrence of the value in a context where + // it is valid. + if (value.length() > 0) { + return findRange(file, line, property, value); + } else { + // Find first occurrence of property followed by '' or "" + IRegion region1 = findRange(file, line, property, "\"\""); //$NON-NLS-1$ + IRegion region2 = findRange(file, line, property, "''"); //$NON-NLS-1$ + if (region1 == null) { + if (region2 == null) { + // Highlight the property instead + return findRange(file, line, property, null); + } + return region2; + } else if (region2 == null) { + return region1; + } else if (region1.getOffset() < region2.getOffset()) { + return region1; + } else { + return region2; + } + } + } + + matcher = sRepeatedRangePattern.matcher(message); + if (matcher.find()) { + String property = matcher.group(2); + return findRange(file, line, property, null); + } + + matcher = sNoResourcePattern.matcher(message); + if (matcher.find()) { + String property = matcher.group(1); + return findRange(file, line, property, null); + } + + matcher = sRequiredPattern.matcher(message); + if (matcher.find()) { + String elementName = matcher.group(2); + IRegion region = findRange(file, line, '<' + elementName, null); + if (region != null && region.getLength() > 1) { + // Skip the opening < + region = new Region(region.getOffset() + 1, region.getLength() - 1); + } + return region; + } + + if (message.endsWith(ORIGINALLY_DEFINED_MSG)) { + return findLineTextRange(file, line); + } + + return null; + } + + /** + * Given a file and line number, return the range of the first match starting on the + * given line. If second is non null, also search for the second string starting at he + * location of the first string. + */ + private static IRegion findRange(IFile file, int line, String first, + String second) { + IRegion region = null; + IDocumentProvider provider = new TextFileDocumentProvider(); + try { + provider.connect(file); + IDocument document = provider.getDocument(file); + if (document != null) { + IRegion lineInfo = document.getLineInformation(line - 1); + int lineStartOffset = lineInfo.getOffset(); + // The aapt errors will be anchored on the line where the + // element starts - which means that with formatting where + // attributes end up on subsequent lines we don't find it on + // the error line indicated by aapt. + // Therefore, search forwards in the document. + FindReplaceDocumentAdapter adapter = + new FindReplaceDocumentAdapter(document); + + region = adapter.find(lineStartOffset, first, + true /*forwardSearch*/, true /*caseSensitive*/, + false /*wholeWord*/, false /*regExSearch*/); + if (region != null && second != null) { + region = adapter.find(region.getOffset() + first.length(), second, + true /*forwardSearch*/, true /*caseSensitive*/, + false /*wholeWord*/, false /*regExSearch*/); + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't find range information for %1$s", file.getName()); + } finally { + provider.disconnect(file); + } + return region; + } + + /** Returns the non-whitespace line range at the given line number. */ + private static IRegion findLineTextRange(IFile file, int line) { + IDocumentProvider provider = new TextFileDocumentProvider(); + try { + provider.connect(file); + IDocument document = provider.getDocument(file); + if (document != null) { + IRegion lineInfo = document.getLineInformation(line - 1); + String lineContents = document.get(lineInfo.getOffset(), lineInfo.getLength()); + int lineBegin = 0; + int lineEnd = lineContents.length()-1; + + for (; lineEnd >= 0; lineEnd--) { + char c = lineContents.charAt(lineEnd); + if (!Character.isWhitespace(c)) { + break; + } + } + lineEnd++; + for (; lineBegin < lineEnd; lineBegin++) { + char c = lineContents.charAt(lineBegin); + if (!Character.isWhitespace(c)) { + break; + } + } + if (lineBegin < lineEnd) { + return new Region(lineInfo.getOffset() + lineBegin, lineEnd - lineBegin); + } + } + } catch (Exception e) { + AdtPlugin.log(e, "Can't find range information for %1$s", file.getName()); + } finally { + provider.disconnect(file); + } + + return null; + } + + /** + * 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 static final Matcher getNextLineMatcher(String[] lines, + int nextIndex, Pattern pattern) { + // unless we can't, because we reached the last line + if (nextIndex == lines.length) { + // we expected a 2nd line, so we flag as error + // and we bail + return null; + } + + Matcher m = pattern.matcher(lines[nextIndex]); + if (m.matches()) { + return m; + } + + return null; + } + + private static 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 != null && r.exists()) { + return r; + } + } + + return null; + } + +} |