Extract positional XML parser into common and fix encoding issues
The XML DOM parser used by the lint CLI driver (which tracks
positions) is needed outside of lint, so pull it out of the lint/cli
project, and refactor it such that it does not directly reference the
lint Position APIs (but can utilize them when subclassed in lint).
In addition, handle non-UTF-8 file encodings. XML files can be encoded
in other character sets, and can specify this via the encoding
attribute in the XML prologue. Until now, the CLI lint runner would
just read the XML file contents in using the default encoding and
parse this. Now there's a new utility method which takes a byte[] and
infers the desired encoding and uses that to convert the byte[] into a
string using the correct encoding. (We can't just pass an InputStream
and let the SAX parser handle this on its own because the XML parser
needs to access the character stream in order to assign correct node
offsets.) This code now also handles the byte order mark more
cleanly.
There are some new unit tests too to check the new encoding, BOM and
offset handling.
Change-Id: Ib0badbbe72172e3408c6d5af2413be51280a7724
diff --git a/common/.settings/org.eclipse.core.resources.prefs b/common/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..d8a1f3c
--- /dev/null
+++ b/common/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,3 @@
+#Thu Jan 05 21:13:52 PST 2012
+eclipse.preferences.version=1
+encoding//tests/src/com/android/util/PositionXmlParserTest.java=UTF-8
diff --git a/common/.settings/org.eclipse.jdt.core.prefs b/common/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..5381a0e
--- /dev/null
+++ b/common/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,93 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/common/.settings/org.moreunit.prefs b/common/.settings/org.moreunit.prefs
new file mode 100644
index 0000000..c0ed4c1
--- /dev/null
+++ b/common/.settings/org.moreunit.prefs
@@ -0,0 +1,5 @@
+#Thu Jan 05 10:46:32 PST 2012
+eclipse.preferences.version=1
+org.moreunit.prefixes=
+org.moreunit.unitsourcefolder=common\:src\:common-tests\:src
+org.moreunit.useprojectsettings=true
diff --git a/common/src/com/android/io/FileWrapper.java b/common/src/com/android/io/FileWrapper.java
index 2859c0d..84a1f3e 100644
--- a/common/src/com/android/io/FileWrapper.java
+++ b/common/src/com/android/io/FileWrapper.java
@@ -85,6 +85,7 @@
super(uri);
}
+ @Override
public InputStream getContents() throws StreamException {
try {
return new FileInputStream(this);
@@ -93,6 +94,7 @@
}
}
+ @Override
public void setContents(InputStream source) throws StreamException {
FileOutputStream fos = null;
try {
@@ -116,6 +118,7 @@
}
}
+ @Override
public OutputStream getOutputStream() throws StreamException {
try {
return new FileOutputStream(this);
@@ -124,10 +127,12 @@
}
}
+ @Override
public PreferredWriteMode getPreferredWriteMode() {
return PreferredWriteMode.OUTPUTSTREAM;
}
+ @Override
public String getOsLocation() {
return getAbsolutePath();
}
@@ -137,10 +142,12 @@
return isFile();
}
+ @Override
public long getModificationStamp() {
return lastModified();
}
+ @Override
public IAbstractFolder getParentFolder() {
String p = this.getParent();
if (p == null) {
diff --git a/common/src/com/android/io/FolderWrapper.java b/common/src/com/android/io/FolderWrapper.java
index 26ed9cf..c29c934 100644
--- a/common/src/com/android/io/FolderWrapper.java
+++ b/common/src/com/android/io/FolderWrapper.java
@@ -81,6 +81,7 @@
super(file.getAbsolutePath());
}
+ @Override
public IAbstractResource[] listMembers() {
File[] files = listFiles();
final int count = files == null ? 0 : files.length;
@@ -100,8 +101,10 @@
return afiles;
}
+ @Override
public boolean hasFile(final String name) {
String[] match = list(new FilenameFilter() {
+ @Override
public boolean accept(IAbstractFolder dir, String filename) {
return name.equals(filename);
}
@@ -110,14 +113,17 @@
return match.length > 0;
}
+ @Override
public IAbstractFile getFile(String name) {
return new FileWrapper(this, name);
}
+ @Override
public IAbstractFolder getFolder(String name) {
return new FolderWrapper(this, name);
}
+ @Override
public IAbstractFolder getParentFolder() {
String p = this.getParent();
if (p == null) {
@@ -126,6 +132,7 @@
return new FolderWrapper(p);
}
+ @Override
public String getOsLocation() {
return getAbsolutePath();
}
@@ -135,6 +142,7 @@
return isDirectory();
}
+ @Override
public String[] list(FilenameFilter filter) {
File[] files = listFiles();
if (files != null && files.length > 0) {
diff --git a/common/src/com/android/resources/Density.java b/common/src/com/android/resources/Density.java
index f838de4..610789a 100644
--- a/common/src/com/android/resources/Density.java
+++ b/common/src/com/android/resources/Density.java
@@ -72,6 +72,7 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
@@ -88,10 +89,12 @@
return "";
}
+ @Override
public String getShortDisplayValue() {
return mDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mDisplayValue;
}
@@ -120,10 +123,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return this != NODPI; // nodpi is not a valid config for devices.
}
diff --git a/common/src/com/android/resources/Keyboard.java b/common/src/com/android/resources/Keyboard.java
index eb99f9b..d6bc80a 100644
--- a/common/src/com/android/resources/Keyboard.java
+++ b/common/src/com/android/resources/Keyboard.java
@@ -53,14 +53,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -89,10 +92,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return true;
}
diff --git a/common/src/com/android/resources/KeyboardState.java b/common/src/com/android/resources/KeyboardState.java
index e3333f5..2eb7e00 100644
--- a/common/src/com/android/resources/KeyboardState.java
+++ b/common/src/com/android/resources/KeyboardState.java
@@ -50,14 +50,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -86,10 +89,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return true;
}
diff --git a/common/src/com/android/resources/Navigation.java b/common/src/com/android/resources/Navigation.java
index d5d9541..f857e5f 100644
--- a/common/src/com/android/resources/Navigation.java
+++ b/common/src/com/android/resources/Navigation.java
@@ -51,14 +51,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -87,10 +90,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return true;
}
diff --git a/common/src/com/android/resources/NavigationState.java b/common/src/com/android/resources/NavigationState.java
index 266d9da..63b8fea 100644
--- a/common/src/com/android/resources/NavigationState.java
+++ b/common/src/com/android/resources/NavigationState.java
@@ -49,14 +49,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -85,10 +88,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return true;
}
diff --git a/common/src/com/android/resources/NightMode.java b/common/src/com/android/resources/NightMode.java
index 2d64316..8fe1dd9 100644
--- a/common/src/com/android/resources/NightMode.java
+++ b/common/src/com/android/resources/NightMode.java
@@ -49,14 +49,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -85,10 +88,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return true;
}
diff --git a/common/src/com/android/resources/ScreenOrientation.java b/common/src/com/android/resources/ScreenOrientation.java
index 56f907b..b18753d 100644
--- a/common/src/com/android/resources/ScreenOrientation.java
+++ b/common/src/com/android/resources/ScreenOrientation.java
@@ -50,14 +50,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -87,10 +90,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return true;
}
diff --git a/common/src/com/android/resources/ScreenRatio.java b/common/src/com/android/resources/ScreenRatio.java
index 2794b6e..bb575b0 100644
--- a/common/src/com/android/resources/ScreenRatio.java
+++ b/common/src/com/android/resources/ScreenRatio.java
@@ -49,14 +49,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -86,10 +89,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return true;
}
diff --git a/common/src/com/android/resources/ScreenSize.java b/common/src/com/android/resources/ScreenSize.java
index b6ffc50..4def540 100644
--- a/common/src/com/android/resources/ScreenSize.java
+++ b/common/src/com/android/resources/ScreenSize.java
@@ -51,14 +51,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -88,10 +91,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return true;
}
diff --git a/common/src/com/android/resources/TouchScreen.java b/common/src/com/android/resources/TouchScreen.java
index 7ee1f0f..7eeeb08 100644
--- a/common/src/com/android/resources/TouchScreen.java
+++ b/common/src/com/android/resources/TouchScreen.java
@@ -50,14 +50,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -87,10 +90,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return false;
}
+ @Override
public boolean isValidValueForDevice() {
return true;
}
diff --git a/common/src/com/android/resources/UiMode.java b/common/src/com/android/resources/UiMode.java
index 36c903b..d1ddbc8 100644
--- a/common/src/com/android/resources/UiMode.java
+++ b/common/src/com/android/resources/UiMode.java
@@ -49,14 +49,17 @@
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mDisplayValue;
}
@@ -85,10 +88,12 @@
return null;
}
+ @Override
public boolean isFakeValue() {
return this == NORMAL; // NORMAL is not a real enum. it's used for internal state only.
}
+ @Override
public boolean isValidValueForDevice() {
return this != NORMAL;
}
diff --git a/common/src/com/android/util/PositionXmlParser.java b/common/src/com/android/util/PositionXmlParser.java
new file mode 100644
index 0000000..bfe8075
--- /dev/null
+++ b/common/src/com/android/util/PositionXmlParser.java
@@ -0,0 +1,675 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.util;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * A simple DOM XML parser which can retrieve exact beginning and end offsets
+ * (and line and column numbers) for element nodes as well as attribute nodes.
+ */
+public class PositionXmlParser {
+ private static final String UTF_8 = "UTF-8"; //$NON-NLS-1$
+ private static final String UTF_16 = "UTF_16"; //$NON-NLS-1$
+ private static final String UTF_16LE = "UTF_16LE"; //$NON-NLS-1$
+ private static final String CONTENT_KEY = "contents"; //$NON-NLS-1$
+ private final static String POS_KEY = "offsets"; //$NON-NLS-1$
+ private static final String NAMESPACE_PREFIX_FEATURE =
+ "http://xml.org/sax/features/namespace-prefixes"; //$NON-NLS-1$
+ private static final String NAMESPACE_FEATURE =
+ "http://xml.org/sax/features/namespaces"; //$NON-NLS-1$
+ /** See http://www.w3.org/TR/REC-xml/#NT-EncodingDecl */
+ private static final Pattern ENCODING_PATTERN =
+ Pattern.compile("encoding=['\"](\\S*)['\"]");//$NON-NLS-1$
+
+ /**
+ * Parses the XML content from the given input stream.
+ *
+ * @param input the input stream containing the XML to be parsed
+ * @return the corresponding document
+ * @throws ParserConfigurationException if a SAX parser is not available
+ * @throws SAXException if the document contains a parsing error
+ * @throws IOException if something is seriously wrong. This should not
+ * happen since the input source is known to be constructed from
+ * a string.
+ */
+ @Nullable
+ public Document parse(@NonNull InputStream input)
+ throws ParserConfigurationException, SAXException, IOException {
+ // Read in all the data
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buf = new byte[1024];
+ while (true) {
+ int r = input.read(buf);
+ if (r == -1) {
+ break;
+ }
+ out.write(buf, 0, r);
+ }
+ input.close();
+ return parse(out.toByteArray());
+ }
+
+ /**
+ * Parses the XML content from the given byte array
+ *
+ * @param data the raw XML data (with unknown encoding)
+ * @return the corresponding document
+ * @throws ParserConfigurationException if a SAX parser is not available
+ * @throws SAXException if the document contains a parsing error
+ * @throws IOException if something is seriously wrong. This should not
+ * happen since the input source is known to be constructed from
+ * a string.
+ */
+ @Nullable
+ public Document parse(@NonNull byte[] data)
+ throws ParserConfigurationException, SAXException, IOException {
+ String xml = getXmlString(data);
+ return parse(xml, new InputSource(new StringReader(xml)), true);
+ }
+
+ /**
+ * Parses the given XML content.
+ *
+ * @param xml the XML string to be parsed. This must be in the correct
+ * encoding already.
+ * @return the corresponding document
+ * @throws ParserConfigurationException if a SAX parser is not available
+ * @throws SAXException if the document contains a parsing error
+ * @throws IOException if something is seriously wrong. This should not
+ * happen since the input source is known to be constructed from
+ * a string.
+ */
+ @Nullable
+ public Document parse(@NonNull String xml)
+ throws ParserConfigurationException, SAXException, IOException {
+ return parse(xml, new InputSource(new StringReader(xml)), true);
+ }
+
+ @NonNull
+ private Document parse(@NonNull String xml, @NonNull InputSource input, boolean checkBom)
+ throws ParserConfigurationException, SAXException, IOException {
+ try {
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setFeature(NAMESPACE_FEATURE, true);
+ factory.setFeature(NAMESPACE_PREFIX_FEATURE, true);
+ SAXParser parser = factory.newSAXParser();
+ DomBuilder handler = new DomBuilder(xml);
+ parser.parse(input, handler);
+ return handler.getDocument();
+ } catch (SAXException e) {
+ if (checkBom && e.getMessage().contains("Content is not allowed in prolog")) {
+ // Byte order mark in the string? Skip it. There are many markers
+ // (see http://en.wikipedia.org/wiki/Byte_order_mark) so here we'll
+ // just skip those up to the XML prolog beginning character, <
+ xml = xml.replaceFirst("^([\\W]+)<","<"); //$NON-NLS-1$ //$NON-NLS-2$
+ return parse(xml, null, false);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Returns the String corresponding to the given byte array of XML data
+ * (with unknown encoding). This method attempts to guess the encoding based
+ * on the XML prologue.
+ * @param data the XML data to be decoded into a string
+ * @return a string corresponding to the XML data
+ */
+ public static String getXmlString(byte[] data) {
+ int offset = 0;
+
+ String defaultCharset = UTF_8;
+ String charset = null;
+ // Look for the byte order mark, to see if we need to remove bytes from
+ // the input stream (and to determine whether files are big endian or little endian) etc
+ // for files which do not specify the encoding.
+ // See http://unicode.org/faq/utf_bom.html#BOM for more.
+ if (data.length > 4) {
+ if (data[0] == (byte)0xef && data[1] == (byte)0xbb && data[2] == (byte)0xbf) {
+ // UTF-8
+ defaultCharset = charset = UTF_8;
+ offset += 3;
+ } else if (data[0] == (byte)0xfe && data[1] == (byte)0xff) {
+ // UTF-16, big-endian
+ defaultCharset = charset = UTF_16;
+ offset += 2;
+ } else if (data[0] == (byte)0x0 && data[1] == (byte)0x0
+ && data[2] == (byte)0xfe && data[3] == (byte)0xff) {
+ // UTF-32, big-endian
+ defaultCharset = charset = "UTF_32"; //$NON-NLS-1$
+ offset += 4;
+ } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe
+ && data[2] == (byte)0x0 && data[3] == (byte)0x0) {
+ // UTF-32, little-endian. We must check for this *before* looking for
+ // UTF_16LE since UTF_32LE has the same prefix!
+ defaultCharset = charset = "UTF_32LE"; //$NON-NLS-1$
+ offset += 4;
+ } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe) {
+ // UTF-16, little-endian
+ defaultCharset = charset = UTF_16LE;
+ offset += 2;
+ }
+ }
+ int length = data.length - offset;
+
+ // Guess encoding by searching for an encoding= entry in the first line.
+ // The prologue, and the encoding names, will always be in ASCII - which means
+ // we don't need to worry about strange character encodings for the prologue characters.
+ // However, one wrinkle is that the whole file may be encoded in something like UTF-16
+ // where there are two bytes per character, so we can't just look for
+ // ['e','n','c','o','d','i','n','g'] etc in the byte array since there could be
+ // multiple bytes for each character. However, since again the prologue is in ASCII,
+ // we can just drop the zeroes.
+ boolean seenOddZero = false;
+ boolean seenEvenZero = false;
+ int prologueStart = -1;
+ for (int lineEnd = offset; lineEnd < data.length; lineEnd++) {
+ if (data[lineEnd] == 0) {
+ if ((lineEnd - offset) % 1 == 0) {
+ seenEvenZero = true;
+ } else {
+ seenOddZero = true;
+ }
+ } else if (data[lineEnd] == '\n') {
+ break;
+ } else if (data[lineEnd] == '<') {
+ prologueStart = lineEnd;
+ } else if (data[lineEnd] == '>') {
+ // End of prologue. Quick check to see if this is a utf-8 file since that's
+ // common
+ for (int i = lineEnd - 4; i >= 0; i--) {
+ if ((data[i] == 'u' || data[i] == 'U')
+ && (data[i + 1] == 't' || data[i + 1] == 'T')
+ && (data[i + 2] == 'f' || data[i + 2] == 'F')
+ && (data[i + 3] == '-' || data[i + 3] == '_')
+ && (data[i + 4] == '8')
+ ) {
+ charset = UTF_8;
+ break;
+ }
+ }
+
+ if (charset == null) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = prologueStart; i <= lineEnd; i++) {
+ if (data[i] != 0) {
+ sb.append((char) data[i]);
+ }
+ }
+ String prologue = sb.toString();
+ int encodingIndex = prologue.indexOf("encoding"); //$NON-NLS-1$
+ if (encodingIndex != -1) {
+ Matcher matcher = ENCODING_PATTERN.matcher(prologue);
+ if (matcher.find(encodingIndex)) {
+ charset = matcher.group(1);
+ }
+ }
+ }
+
+ break;
+ }
+ }
+
+ // No prologue on the first line, and no byte order mark: Assume UTF-8/16
+ if (charset == null) {
+ charset = seenOddZero ? UTF_16 : seenEvenZero ? UTF_16LE : UTF_8;
+ }
+
+ String xml = null;
+ try {
+ xml = new String(data, offset, length, charset);
+ } catch (UnsupportedEncodingException e) {
+ try {
+ if (charset != defaultCharset) {
+ xml = new String(data, offset, length, defaultCharset);
+ }
+ } catch (UnsupportedEncodingException u) {
+ // Just use the default encoding below
+ }
+ }
+ if (xml == null) {
+ xml = new String(data, offset, length);
+ }
+ return xml;
+ }
+
+ /**
+ * Returns the position for the given node. This is the start position. The
+ * end position can be obtained via {@link Position#getEnd()}.
+ *
+ * @param node the node to look up position for
+ * @return the position, or null if the node type is not supported for
+ * position info
+ */
+ @Nullable
+ public Position getPosition(@NonNull Node node) {
+ // Look up the position information stored while parsing for the given node.
+ // Note however that we only store position information for elements (because
+ // there is no SAX callback for individual attributes).
+ // Therefore, this method special cases this:
+ // -- First, it looks at the owner element and uses its position
+ // information as a first approximation.
+ // -- Second, it uses that, as well as the original XML text, to search
+ // within the node range for an exact text match on the attribute name
+ // and if found uses that as the exact node offsets instead.
+ if (node instanceof Attr) {
+ Attr attr = (Attr) node;
+ Position pos = (Position) attr.getOwnerElement().getUserData(POS_KEY);
+ if (pos != null) {
+ int startOffset = pos.getOffset();
+ int endOffset = pos.getEnd().getOffset();
+
+ // Find attribute in the text
+ String contents = (String) node.getOwnerDocument().getUserData(CONTENT_KEY);
+ if (contents == null) {
+ return null;
+ }
+
+ // Locate the name=value attribute in the source text
+ // Fast string check first for the common occurrence
+ String name = attr.getName();
+ Pattern pattern = Pattern.compile(
+ String.format("%1$s\\s*=\\s*[\"'].*[\"']", name)); //$NON-NLS-1$
+ Matcher matcher = pattern.matcher(contents);
+ if (matcher.find(startOffset) && matcher.start() <= endOffset) {
+ int index = matcher.start();
+ // Adjust the line and column to this new offset
+ int line = pos.getLine();
+ int column = pos.getColumn();
+ for (int offset = pos.getOffset(); offset < index; offset++) {
+ char t = contents.charAt(offset);
+ if (t == '\n') {
+ line++;
+ column = 0;
+ } else {
+ column++;
+ }
+ }
+
+ Position attributePosition = createPosition(line, column, index);
+ // Also set end range for retrieval in getLocation
+ attributePosition.setEnd(createPosition(line, column, matcher.end()));
+ return attributePosition;
+ } else {
+ // No regexp match either: just fall back to element position
+ return pos;
+ }
+ }
+ } else if (node instanceof Text) {
+ // Position of parent element, if any
+ Position pos = null;
+ if (node.getPreviousSibling() != null) {
+ pos = (Position) node.getPreviousSibling().getUserData(POS_KEY);
+ }
+ if (pos == null) {
+ pos = (Position) node.getParentNode().getUserData(POS_KEY);
+ }
+ if (pos != null) {
+ // Attempt to point forward to the actual text node
+ int startOffset = pos.getOffset();
+ int endOffset = pos.getEnd().getOffset();
+ int line = pos.getLine();
+ int column = pos.getColumn();
+
+ // Find attribute in the text
+ String contents = (String) node.getOwnerDocument().getUserData(CONTENT_KEY);
+ if (contents == null || contents.length() < endOffset) {
+ return null;
+ }
+
+ boolean inAttribute = false;
+ for (int offset = startOffset; offset <= endOffset; offset++) {
+ char c = contents.charAt(offset);
+ if (c == '>' && !inAttribute) {
+ // Found the end of the element open tag: this is where the
+ // text begins.
+
+ // Skip >
+ offset++;
+
+ // Skip text whitespace prefix, if the text node contains non-whitespace
+ // characters
+ String text = node.getNodeValue();
+ int textIndex = 0;
+ int textLength = text.length();
+ int newLine = line;
+ int newColumn = column;
+ for (; textIndex < text.length(); textIndex++) {
+ char t = text.charAt(textIndex);
+ if (t == '\n') {
+ newLine++;
+ newColumn = 0;
+ } else {
+ newColumn++;
+ }
+ if (!Character.isWhitespace(t)) {
+ break;
+ }
+ }
+ if (textIndex == textLength) {
+ textIndex = 0; // Whitespace node
+ } else {
+ line = newLine;
+ column = newColumn;
+ }
+
+ Position attributePosition = createPosition(line, column,
+ offset);
+ // Also set end range for retrieval in getLocation
+ attributePosition.setEnd(createPosition(line, column,
+ offset + textLength));
+ return attributePosition;
+ } else if (c == '"') {
+ inAttribute = !inAttribute;
+ } else if (c == '\n') {
+ line++;
+ column = -1; // pre-subtract column added below
+ }
+ column++;
+ }
+
+ return pos;
+ }
+ }
+
+ return (Position) node.getUserData(POS_KEY);
+ }
+
+ /**
+ * SAX parser handler which incrementally builds up a DOM document as we go
+ * along, and updates position information along the way. Position
+ * information is attached to the DOM nodes by setting user data with the
+ * {@link POS_KEY} key.
+ */
+ private final class DomBuilder extends DefaultHandler {
+ private final String mXml;
+ private final Document mDocument;
+ private Locator mLocator;
+ private int mCurrentLine = 0;
+ private int mCurrentOffset;
+ private int mCurrentColumn;
+ private final List<Element> mStack = new ArrayList<Element>();
+ private final StringBuilder mPendingText = new StringBuilder();
+
+ private DomBuilder(String xml) throws ParserConfigurationException {
+ mXml = xml;
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ DocumentBuilder docBuilder = factory.newDocumentBuilder();
+ mDocument = docBuilder.newDocument();
+ mDocument.setUserData(CONTENT_KEY, xml, null);
+ }
+
+ /** Returns the document parsed by the handler */
+ Document getDocument() {
+ return mDocument;
+ }
+
+ @Override
+ public void setDocumentLocator(Locator locator) {
+ this.mLocator = locator;
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+ flushText();
+ Element element = mDocument.createElement(qName);
+ for (int i = 0; i < attributes.getLength(); i++) {
+ if (attributes.getURI(i) != null && attributes.getURI(i).length() > 0) {
+ Attr attr = mDocument.createAttributeNS(attributes.getURI(i),
+ attributes.getQName(i));
+ attr.setValue(attributes.getValue(i));
+ element.setAttributeNodeNS(attr);
+ assert attr.getOwnerElement() == element;
+ } else {
+ Attr attr = mDocument.createAttribute(attributes.getQName(i));
+ attr.setValue(attributes.getValue(i));
+ element.setAttributeNode(attr);
+ assert attr.getOwnerElement() == element;
+ }
+ }
+
+ Position pos = getCurrentPosition();
+
+ // The starting position reported to us by SAX is really the END of the
+ // open tag in an element, when all the attributes have been processed.
+ // We have to scan backwards to find the real beginning. We'll do that
+ // by scanning backwards.
+ // -1: Make sure that when we have <foo></foo> we don't consider </foo>
+ // the beginning since pos.offset will typically point to the first character
+ // AFTER the element open tag, which could be a closing tag or a child open
+ // tag
+
+ for (int offset = pos.getOffset() - 1; offset >= 0; offset--) {
+ char c = mXml.charAt(offset);
+ // < cannot appear in attribute values or anywhere else within
+ // an element open tag, so we know the first occurrence is the real
+ // element start
+ if (c == '<') {
+ // Adjust line position
+ int line = pos.getLine();
+ for (int i = offset, n = pos.getOffset(); i < n; i++) {
+ if (mXml.charAt(i) == '\n') {
+ line--;
+ }
+ }
+
+ // Compute new column position
+ int column = 0;
+ for (int i = offset; i >= 0; i--, column++) {
+ if (mXml.charAt(i) == '\n') {
+ break;
+ }
+ }
+
+ pos = createPosition(line, column, offset);
+ break;
+ }
+ }
+
+ element.setUserData(POS_KEY, pos, null);
+ mStack.add(element);
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName) {
+ flushText();
+ Element element = mStack.remove(mStack.size() - 1);
+
+ Position pos = (Position) element.getUserData(POS_KEY);
+ assert pos != null;
+ pos.setEnd(getCurrentPosition());
+
+ if (mStack.isEmpty()) {
+ mDocument.appendChild(element);
+ } else {
+ Element parent = mStack.get(mStack.size() - 1);
+ parent.appendChild(element);
+ }
+ }
+
+ /**
+ * Returns a position holder for the current position. The most
+ * important part of this function is to incrementally compute the
+ * offset as well, by counting forwards until it reaches the new line
+ * number and column position of the XML parser, counting characters as
+ * it goes along.
+ */
+ private Position getCurrentPosition() {
+ int line = mLocator.getLineNumber() - 1;
+ int column = mLocator.getColumnNumber() - 1;
+
+ // Compute offset incrementally now that we have the new line and column
+ // numbers
+ while (mCurrentLine < line) {
+ char c = mXml.charAt(mCurrentOffset);
+ if (c == '\r' && mCurrentOffset < mXml.length() - 1) {
+ if (mXml.charAt(mCurrentOffset + 1) != '\n') {
+ mCurrentLine++;
+ mCurrentColumn = 0;
+ }
+ } else if (c == '\n') {
+ mCurrentLine++;
+ mCurrentColumn = 0;
+ } else {
+ mCurrentColumn++;
+ }
+ mCurrentOffset++;
+ }
+
+ mCurrentOffset += column - mCurrentColumn;
+ mCurrentColumn = column;
+
+ return createPosition(mCurrentLine, mCurrentColumn, mCurrentOffset);
+ }
+
+ @Override
+ public void characters(char c[], int start, int length) throws SAXException {
+ mPendingText.append(c, start, length);
+ }
+
+ private void flushText() {
+ if (mPendingText.length() > 0 && !mStack.isEmpty()) {
+ Element element = mStack.get(mStack.size() - 1);
+ Node textNode = mDocument.createTextNode(mPendingText.toString());
+ element.appendChild(textNode);
+ mPendingText.setLength(0);
+ }
+ }
+ }
+
+ /**
+ * Creates a position while constructing the DOM document. This method
+ * allows a subclass to create a custom implementation of the position
+ * class.
+ *
+ * @param line the line number for the position
+ * @param column the column number for the position
+ * @param offset the character offset
+ * @return a new position
+ */
+ @NonNull
+ protected Position createPosition(int line, int column, int offset) {
+ return new DefaultPosition(line, column, offset);
+ }
+
+ protected interface Position {
+ /**
+ * Linked position: for a begin position this will point to the
+ * corresponding end position. For an end position this will be null.
+ *
+ * @return the end position, or null
+ */
+ @Nullable
+ public Position getEnd();
+
+ /**
+ * Linked position: for a begin position this will point to the
+ * corresponding end position. For an end position this will be null.
+ *
+ * @param end the end position
+ */
+ public void setEnd(@NonNull Position end);
+
+ /** @return the line number, 0-based */
+ public int getLine();
+
+ /** @return the offset number, 0-based */
+ public int getOffset();
+
+ /** @return the column number, 0-based, and -1 if the column number if not known */
+ public int getColumn();
+ }
+
+ protected static class DefaultPosition implements Position {
+ /** The line number (0-based where the first line is line 0) */
+ private final int mLine;
+ private final int mColumn;
+ private final int mOffset;
+ private Position mEnd;
+
+ /**
+ * Creates a new {@link Position}
+ *
+ * @param line the 0-based line number, or -1 if unknown
+ * @param column the 0-based column number, or -1 if unknown
+ * @param offset the offset, or -1 if unknown
+ */
+ public DefaultPosition(int line, int column, int offset) {
+ this.mLine = line;
+ this.mColumn = column;
+ this.mOffset = offset;
+ }
+
+ @Override
+ public int getLine() {
+ return mLine;
+ }
+
+ @Override
+ public int getOffset() {
+ return mOffset;
+ }
+
+ @Override
+ public int getColumn() {
+ return mColumn;
+ }
+
+ @Override
+ public Position getEnd() {
+ return mEnd;
+ }
+
+ @Override
+ public void setEnd(Position end) {
+ mEnd = end;
+ }
+ }
+}
diff --git a/common/tests/.classpath b/common/tests/.classpath
index b793adc..15b6472 100644
--- a/common/tests/.classpath
+++ b/common/tests/.classpath
@@ -4,5 +4,6 @@
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/>
<classpathentry combineaccessrules="false" kind="src" path="/common"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-10.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src.zip"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/common/tests/src/com/android/util/PositionXmlParserTest.java b/common/tests/src/com/android/util/PositionXmlParserTest.java
new file mode 100644
index 0000000..9f87252
--- /dev/null
+++ b/common/tests/src/com/android/util/PositionXmlParserTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.util;
+
+import com.android.util.PositionXmlParser.Position;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+import junit.framework.TestCase;
+
+@SuppressWarnings("javadoc")
+public class PositionXmlParserTest extends TestCase {
+ public void test() throws Exception {
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
+ " android:layout_width=\"match_parent\"\n" +
+ " android:layout_height=\"wrap_content\"\n" +
+ " android:orientation=\"vertical\" >\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button1\"\n" +
+ " android:layout_width=\"wrap_content\"\n" +
+ " android:layout_height=\"wrap_content\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ " <Button\n" +
+ " android:id=\"@+id/button2\"\n" +
+ " android:layout_width=\"wrap_content\"\n" +
+ " android:layout_height=\"wrap_content\"\n" +
+ " android:text=\"Button\" />\n" +
+ "\n" +
+ "</LinearLayout>\n";
+ PositionXmlParser parser = new PositionXmlParser();
+ File file = File.createTempFile("parsertest", ".xml");
+ Writer fw = new BufferedWriter(new FileWriter(file));
+ fw.write(xml);
+ fw.close();
+ Document document = parser.parse(new FileInputStream(file));
+ assertNotNull(document);
+
+ // Basic parsing heart beat tests
+ Element linearLayout = (Element) document.getElementsByTagName("LinearLayout").item(0);
+ assertNotNull(linearLayout);
+ NodeList buttons = document.getElementsByTagName("Button");
+ assertEquals(2, buttons.getLength());
+ final String ANDROID_URI = "http://schemas.android.com/apk/res/android";
+ assertEquals("wrap_content",
+ linearLayout.getAttributeNS(ANDROID_URI, "layout_height"));
+
+ // Check attribute positions
+ Attr attr = linearLayout.getAttributeNodeNS(ANDROID_URI, "layout_width");
+ assertNotNull(attr);
+ Position start = parser.getPosition(attr);
+ Position end = start.getEnd();
+ assertEquals(2, start.getLine());
+ assertEquals(xml.indexOf("android:layout_width"), start.getOffset());
+ assertEquals(2, end.getLine());
+ String target = "android:layout_width=\"match_parent\"";
+ assertEquals(xml.indexOf(target) + target.length(), end.getOffset());
+
+ // Check element positions
+ Element button = (Element) buttons.item(0);
+ start = parser.getPosition(button);
+ end = start.getEnd();
+ assertNull(end.getEnd());
+ assertEquals(6, start.getLine());
+ assertEquals(xml.indexOf("<Button"), start.getOffset());
+ assertEquals(xml.indexOf("/>") + 2, end.getOffset());
+ assertEquals(10, end.getLine());
+ int button1End = end.getOffset();
+
+ Element button2 = (Element) buttons.item(1);
+ start = parser.getPosition(button2);
+ end = start.getEnd();
+ assertEquals(12, start.getLine());
+ assertEquals(xml.indexOf("<Button", button1End), start.getOffset());
+ assertEquals(xml.indexOf("/>", start.getOffset()) + 2, end.getOffset());
+ assertEquals(16, end.getLine());
+
+ file.delete();
+ }
+
+ public void testLineEndings() throws Exception {
+ // Test for http://code.google.com/p/android/issues/detail?id=22925
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" +
+ "<LinearLayout>\r\n" +
+ "\r" +
+ "<LinearLayout></LinearLayout>\r\n" +
+ "</LinearLayout>\r\n";
+ PositionXmlParser parser = new PositionXmlParser();
+ File file = File.createTempFile("parsertest2", ".xml");
+ Writer fw = new BufferedWriter(new FileWriter(file));
+ fw.write(xml);
+ fw.close();
+ Document document = parser.parse(new FileInputStream(file));
+ assertNotNull(document);
+
+ file.delete();
+ }
+
+ private static void checkEncoding(String encoding, boolean writeBom, boolean writeEncoding)
+ throws Exception {
+ String value = "¾¿ÂŒ";
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("<?xml version=\"1.0\"");
+ if (writeEncoding) {
+ sb.append(" encoding=\"");
+ sb.append(encoding);
+ sb.append("\"");
+ }
+ sb.append("?>\n" +
+ "<!-- This is a \n" +
+ " multiline comment\n" +
+ "-->\n" +
+ "<foo ");
+ int startAttrOffset = sb.length();
+ sb.append("attr=\"");
+ sb.append(value);
+ sb.append("\"");
+ sb.append(">\n" +
+ "\n" +
+ "<bar></bar>\n" +
+ "</foo>\n");
+ PositionXmlParser parser = new PositionXmlParser();
+ File file = File.createTempFile("parsertest" + encoding + writeBom + writeEncoding,
+ ".xml");
+ BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(file));
+ OutputStreamWriter writer = new OutputStreamWriter(stream, encoding);
+
+ if (writeBom) {
+ String normalized = encoding.toLowerCase().replace("-", "_");
+ if (normalized.equals("utf_8")) {
+ stream.write(0xef);
+ stream.write(0xbb);
+ stream.write(0xbf);
+ } else if (normalized.equals("utf_16")) {
+ stream.write(0xfe);
+ stream.write(0xff);
+ } else if (normalized.equals("utf_16le")) {
+ stream.write(0xff);
+ stream.write(0xfe);
+ } else if (normalized.equals("utf_32")) {
+ stream.write(0x0);
+ stream.write(0x0);
+ stream.write(0xfe);
+ stream.write(0xff);
+ } else if (normalized.equals("utf_32le")) {
+ stream.write(0xff);
+ stream.write(0xfe);
+ stream.write(0x0);
+ stream.write(0x0);
+ } else {
+ fail("Can't write BOM for encoding " + encoding);
+ }
+ }
+
+ writer.write(sb.toString());
+ writer.close();
+
+ Document document = parser.parse(new FileInputStream(file));
+ assertNotNull(document);
+ Element root = document.getDocumentElement();
+ assertEquals(file.getPath(), value, root.getAttribute("attr"));
+ assertEquals(4, parser.getPosition(root).getLine());
+
+ Attr attribute = root.getAttributeNode("attr");
+ assertNotNull(attribute);
+ Position position = parser.getPosition(attribute);
+ assertNotNull(position);
+ assertEquals(4, position.getLine());
+ assertEquals(startAttrOffset, position.getOffset());
+
+ file.delete();
+ }
+
+ public void testEncoding() throws Exception {
+ checkEncoding("utf-8", false /*bom*/, true /*encoding*/);
+ checkEncoding("UTF-8", false /*bom*/, true /*encoding*/);
+ checkEncoding("UTF_16", false /*bom*/, true /*encoding*/);
+ checkEncoding("UTF-16", false /*bom*/, true /*encoding*/);
+ checkEncoding("UTF_16LE", false /*bom*/, true /*encoding*/);
+ checkEncoding("UTF_32", false /*bom*/, true /*encoding*/);
+ checkEncoding("UTF_32LE", false /*bom*/, true /*encoding*/);
+ checkEncoding("windows-1252", false /*bom*/, true /*encoding*/);
+ checkEncoding("MacRoman", false /*bom*/, true /*encoding*/);
+ checkEncoding("ISO-8859-1", false /*bom*/, true /*encoding*/);
+ checkEncoding("iso-8859-1", false /*bom*/, true /*encoding*/);
+
+ // Try BOM's (with no encoding specified)
+ checkEncoding("utf-8", true /*bom*/, false /*encoding*/);
+ checkEncoding("UTF-8", true /*bom*/, false /*encoding*/);
+ checkEncoding("UTF_16", true /*bom*/, false /*encoding*/);
+ checkEncoding("UTF-16", true /*bom*/, false /*encoding*/);
+ checkEncoding("UTF_16LE", true /*bom*/, false /*encoding*/);
+ checkEncoding("UTF_32", true /*bom*/, false /*encoding*/);
+ checkEncoding("UTF_32LE", true /*bom*/, false /*encoding*/);
+
+ // Try default encodings (only defined for utf-8 and utf-16)
+ checkEncoding("utf-8", false /*bom*/, false /*encoding*/);
+ checkEncoding("UTF-8", false /*bom*/, false /*encoding*/);
+ checkEncoding("UTF_16", false /*bom*/, false /*encoding*/);
+ checkEncoding("UTF-16", false /*bom*/, false /*encoding*/);
+ checkEncoding("UTF_16LE", false /*bom*/, false /*encoding*/);
+
+ // Try BOM's (with explicit encoding specified)
+ checkEncoding("utf-8", true /*bom*/, true /*encoding*/);
+ checkEncoding("UTF-8", true /*bom*/, true /*encoding*/);
+ checkEncoding("UTF_16", true /*bom*/, true /*encoding*/);
+ checkEncoding("UTF-16", true /*bom*/, true /*encoding*/);
+ checkEncoding("UTF_16LE", true /*bom*/, true /*encoding*/);
+ checkEncoding("UTF_32", true /*bom*/, true /*encoding*/);
+ checkEncoding("UTF_32LE", true /*bom*/, true /*encoding*/);
+ }
+}
diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt
index c7e5331..601d9eb 100644
--- a/eclipse/dictionary.txt
+++ b/eclipse/dictionary.txt
@@ -85,6 +85,7 @@
dropdown
ed
editable
+endian
endpoint
enum
enums
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java
index d105867..ba5b63e 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java
@@ -38,6 +38,8 @@
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.util.Pair;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
@@ -64,9 +66,7 @@
import org.w3c.dom.Document;
import org.w3c.dom.Node;
-import java.io.BufferedReader;
import java.io.File;
-import java.io.FileReader;
import java.io.IOException;
/**
@@ -502,33 +502,12 @@
return readPlainFile(f);
}
- private String readPlainFile(File f) {
- // TODO: Connect to document and read live contents
- BufferedReader reader = null;
+ private String readPlainFile(File file) {
try {
- reader = new BufferedReader(new FileReader(f));
- StringBuilder sb = new StringBuilder((int) f.length());
- while (true) {
- int c = reader.read();
- if (c == -1) {
- return sb.toString();
- } else {
- sb.append((char)c);
- }
- }
+ return Files.toString(file, Charsets.UTF_8);
} catch (IOException e) {
- // pass -- ignore files we can't read
- } finally {
- try {
- if (reader != null) {
- reader.close();
- }
- } catch (IOException e) {
- log(e, null);
- }
+ return ""; //$NON-NLS-1$
}
-
- return ""; //$NON-NLS-1$
}
@Override
diff --git a/lint/cli/.classpath b/lint/cli/.classpath
index 267ebe8..362d028 100644
--- a/lint/cli/.classpath
+++ b/lint/cli/.classpath
@@ -2,6 +2,7 @@
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/common"/>
<classpathentry combineaccessrules="false" kind="src" path="/lint-api"/>
<classpathentry combineaccessrules="false" kind="src" path="/lint-checks"/>
<classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/asm-tools/asm-4.0.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/asm-tools/src.zip"/>
diff --git a/lint/cli/Android.mk b/lint/cli/Android.mk
index 3aec3e2..a78a9fe 100644
--- a/lint/cli/Android.mk
+++ b/lint/cli/Android.mk
@@ -10,6 +10,7 @@
# If the dependency list is changed, etc/manifest.txt
LOCAL_JAVA_LIBRARIES := \
+ common \
lint_api \
lint_checks
LOCAL_STATIC_JAVA_LIBRARIES := \
diff --git a/lint/cli/etc/manifest.txt b/lint/cli/etc/manifest.txt
index a60c7ca..242c7f4 100644
--- a/lint/cli/etc/manifest.txt
+++ b/lint/cli/etc/manifest.txt
@@ -1,2 +1,2 @@
Main-Class: com.android.tools.lint.Main
-Class-Path: lint_api.jar lint_checks.jar asm-4.0.jar asm-tree-4.0.jar guava-10.0.1.jar
+Class-Path: common.jar lint_api.jar lint_checks.jar asm-4.0.jar asm-tree-4.0.jar guava-10.0.1.jar
diff --git a/lint/cli/src/com/android/tools/lint/LintCliXmlParser.java b/lint/cli/src/com/android/tools/lint/LintCliXmlParser.java
new file mode 100644
index 0000000..1c7b13a
--- /dev/null
+++ b/lint/cli/src/com/android/tools/lint/LintCliXmlParser.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.tools.lint;
+
+import com.android.tools.lint.client.api.IDomParser;
+import com.android.tools.lint.client.api.IssueRegistry;
+import com.android.tools.lint.detector.api.Location;
+import com.android.tools.lint.detector.api.Location.Handle;
+import com.android.tools.lint.detector.api.XmlContext;
+import com.android.util.PositionXmlParser;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * A customization of the {@link PositionXmlParser} which creates position
+ * objects that directly extend the lint
+ * {@link com.android.tools.lint.detector.api.Position} class.
+ * <p>
+ * It also catches and reports parser errors as lint errors.
+ */
+public class LintCliXmlParser extends PositionXmlParser implements IDomParser {
+ @Override
+ public Document parseXml(XmlContext context) {
+ try {
+ // Do we need to provide an input stream for encoding?
+ String xml = context.getContents();
+ if (xml != null) {
+ return super.parse(xml);
+ }
+ } catch (UnsupportedEncodingException e) {
+ context.report(
+ // Must provide an issue since API guarantees that the issue parameter
+ // is valid
+ IssueRegistry.PARSER_ERROR, Location.create(context.file),
+ e.getCause() != null ? e.getCause().getLocalizedMessage() :
+ e.getLocalizedMessage(),
+ null);
+ } catch (SAXException e) {
+ context.report(
+ // Must provide an issue since API guarantees that the issue parameter
+ // is valid
+ IssueRegistry.PARSER_ERROR, Location.create(context.file),
+ e.getCause() != null ? e.getCause().getLocalizedMessage() :
+ e.getLocalizedMessage(),
+ null);
+ } catch (Throwable t) {
+ context.log(t, null);
+ }
+ return null;
+ }
+
+ @Override
+ public Location getLocation(XmlContext context, Node node) {
+ OffsetPosition pos = (OffsetPosition) getPosition(node);
+ if (pos != null) {
+ return Location.create(context.file, pos, (OffsetPosition) pos.getEnd());
+ }
+
+ return null;
+ }
+
+ @Override
+ public Handle createLocationHandle(XmlContext context, Node node) {
+ return new LocationHandle(context.file, node);
+ }
+
+ @Override
+ protected OffsetPosition createPosition(int line, int column, int offset) {
+ return new OffsetPosition(line, column, offset);
+ }
+
+ private static class OffsetPosition extends com.android.tools.lint.detector.api.Position
+ implements PositionXmlParser.Position {
+ /** The line number (0-based where the first line is line 0) */
+ private final int mLine;
+
+ /**
+ * The column number (where the first character on the line is 0), or -1 if
+ * unknown
+ */
+ private final int mColumn;
+
+ /** The character offset */
+ private final int mOffset;
+
+ /**
+ * Linked position: for a begin offset this will point to the end
+ * offset, and for an end offset this will be null
+ */
+ private com.android.util.PositionXmlParser.Position mEnd;
+
+ /**
+ * Creates a new {@link OffsetPosition}
+ *
+ * @param line the 0-based line number, or -1 if unknown
+ * @param column the 0-based column number, or -1 if unknown
+ * @param offset the offset, or -1 if unknown
+ */
+ public OffsetPosition(int line, int column, int offset) {
+ this.mLine = line;
+ this.mColumn = column;
+ this.mOffset = offset;
+ }
+
+ @Override
+ public int getLine() {
+ return mLine;
+ }
+
+ @Override
+ public int getOffset() {
+ return mOffset;
+ }
+
+ @Override
+ public int getColumn() {
+ return mColumn;
+ }
+
+ @Override
+ public com.android.util.PositionXmlParser.Position getEnd() {
+ return mEnd;
+ }
+
+ @Override
+ public void setEnd(com.android.util.PositionXmlParser.Position end) {
+ mEnd = end;
+ }
+ }
+
+ @Override
+ public void dispose(XmlContext context, Document document) {
+ }
+
+ /* Handle for creating DOM positions cheaply and returning full fledged locations later */
+ private class LocationHandle implements Handle {
+ private File mFile;
+ private Node mNode;
+
+ public LocationHandle(File file, Node node) {
+ mFile = file;
+ mNode = node;
+ }
+
+ @Override
+ public Location resolve() {
+ OffsetPosition pos = (OffsetPosition) getPosition(mNode);
+ if (pos != null) {
+ return Location.create(mFile, pos, (OffsetPosition) pos.getEnd());
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/lint/cli/src/com/android/tools/lint/Main.java b/lint/cli/src/com/android/tools/lint/Main.java
index 489ac81..e37f9c4 100644
--- a/lint/cli/src/com/android/tools/lint/Main.java
+++ b/lint/cli/src/com/android/tools/lint/Main.java
@@ -34,10 +34,11 @@
import com.android.tools.lint.detector.api.Position;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Severity;
+import com.android.util.PositionXmlParser;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
-import java.io.BufferedReader;
import java.io.File;
-import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
@@ -661,7 +662,7 @@
@Override
public IDomParser getDomParser() {
- return new PositionXmlParser();
+ return new LintCliXmlParser();
}
@Override
@@ -790,31 +791,17 @@
@Override
public String readFile(File file) {
- BufferedReader reader = null;
try {
- reader = new BufferedReader(new FileReader(file));
- StringBuilder sb = new StringBuilder((int) file.length());
- while (true) {
- int c = reader.read();
- if (c == -1) {
- return sb.toString();
- } else {
- sb.append((char)c);
- }
+ // For XML files, apply special logic to pick up encoding information within the file
+ if (endsWith(file.getName(), DOT_XML)) {
+ byte[] data = Files.toByteArray(file);
+ return PositionXmlParser.getXmlString(data);
}
- } catch (IOException e) {
- // pass -- ignore files we can't read
- } finally {
- try {
- if (reader != null) {
- reader.close();
- }
- } catch (IOException e) {
- log(e, null);
- }
- }
- return ""; //$NON-NLS-1$
+ return Files.toString(file, Charsets.UTF_8);
+ } catch (IOException e) {
+ return ""; //$NON-NLS-1$
+ }
}
/**
diff --git a/lint/cli/src/com/android/tools/lint/PositionXmlParser.java b/lint/cli/src/com/android/tools/lint/PositionXmlParser.java
deleted file mode 100644
index b4af282..0000000
--- a/lint/cli/src/com/android/tools/lint/PositionXmlParser.java
+++ /dev/null
@@ -1,490 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
- *
- * 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.tools.lint;
-
-import com.android.tools.lint.client.api.IDomParser;
-import com.android.tools.lint.client.api.IssueRegistry;
-import com.android.tools.lint.detector.api.Context;
-import com.android.tools.lint.detector.api.Location;
-import com.android.tools.lint.detector.api.Location.Handle;
-import com.android.tools.lint.detector.api.Position;
-import com.android.tools.lint.detector.api.XmlContext;
-
-import org.w3c.dom.Attr;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.Text;
-import org.xml.sax.Attributes;
-import org.xml.sax.InputSource;
-import org.xml.sax.Locator;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
-
-import java.io.File;
-import java.io.StringReader;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-
-/**
- * A simple DOM XML parser which can retrieve exact beginning and end offsets
- * (and line and column numbers) for element nodes as well as attribute nodes.
- */
-public class PositionXmlParser implements IDomParser {
- private static final String CONTENT_KEY = "contents"; //$NON-NLS-1$
- private final static String POS_KEY = "offsets"; //$NON-NLS-1$
- private static final String NAMESPACE_PREFIX_FEATURE =
- "http://xml.org/sax/features/namespace-prefixes"; //$NON-NLS-1$
- private static final String NAMESPACE_FEATURE =
- "http://xml.org/sax/features/namespaces"; //$NON-NLS-1$
-
- // ---- Implements IDomParser ----
-
- @Override
- public Document parseXml(XmlContext context) {
- return parse(context, context.getContents(), true);
- }
-
- private Document parse(Context context, String xml, boolean checkBom) {
- try {
- SAXParserFactory factory = SAXParserFactory.newInstance();
- factory.setFeature(NAMESPACE_FEATURE, true);
- factory.setFeature(NAMESPACE_PREFIX_FEATURE, true);
- SAXParser parser = factory.newSAXParser();
-
- InputSource input = new InputSource(new StringReader(xml));
- DomBuilder handler = new DomBuilder(xml);
- parser.parse(input, handler);
- return handler.getDocument();
- } catch (ParserConfigurationException e) {
- context.log(e, null);
- } catch (SAXException e) {
- if (checkBom && e.getMessage().contains("Content is not allowed in prolog")) {
- // Byte order mark in the string? Skip it. There are many markers
- // (see http://en.wikipedia.org/wiki/Byte_order_mark) so here we'll
- // just skip those up to the XML prolog beginning character, <
- xml = xml.replaceFirst("^([\\W]+)<","<"); //$NON-NLS-1$ //$NON-NLS-2$
- return parse(context, xml, false);
- }
- context.report(
- // Must provide an issue since API guarantees that the issue parameter
- // is valid
- IssueRegistry.PARSER_ERROR, Location.create(context.file),
- e.getCause() != null ? e.getCause().getLocalizedMessage() :
- e.getLocalizedMessage(),
- null);
- } catch (Throwable t) {
- context.log(t, null);
- }
- return null;
- }
-
- static Position getPositions(Node node) {
- // Look up the position information stored while parsing for the given node.
- // Note however that we only store position information for elements (because
- // there is no SAX callback for individual attributes).
- // Therefore, this method special cases this:
- // -- First, it looks at the owner element and uses its position
- // information as a first approximation.
- // -- Second, it uses that, as well as the original XML text, to search
- // within the node range for an exact text match on the attribute name
- // and if found uses that as the exact node offsets instead.
- if (node instanceof Attr) {
- Attr attr = (Attr) node;
- OffsetPosition pos = (OffsetPosition) attr.getOwnerElement().getUserData(POS_KEY);
- if (pos != null) {
- int startOffset = pos.getOffset();
- int endOffset = pos.next.getOffset();
-
- // Find attribute in the text
- String contents = (String) node.getOwnerDocument().getUserData(CONTENT_KEY);
- if (contents == null) {
- return null;
- }
-
- // Locate the name=value attribute in the source text
- // Fast string check first for the common occurrence
- String name = attr.getName();
- Pattern pattern = Pattern.compile(
- String.format("%1$s\\s*=\\s*[\"'].*[\"']", name)); //$NON-NLS-1$
- Matcher matcher = pattern.matcher(contents);
- if (matcher.find(startOffset) && matcher.start() <= endOffset) {
- int index = matcher.start();
- // Adjust the line and column to this new offset
- int line = pos.getLine();
- int column = pos.getColumn();
- for (int offset = pos.getOffset(); offset < index; offset++) {
- char t = contents.charAt(offset);
- if (t == '\n') {
- line++;
- column = 0;
- } else {
- column++;
- }
- }
-
- OffsetPosition attributePosition = new OffsetPosition(line, column, index);
- // Also set end range for retrieval in getLocation
- attributePosition.next = new OffsetPosition(line, column, matcher.end());
- return attributePosition;
- } else {
- // No regexp match either: just fall back to element position
- return pos;
- }
- }
- } else if (node instanceof Text) {
- // Position of parent element, if any
- OffsetPosition pos = null;
- if (node.getPreviousSibling() != null) {
- pos = (OffsetPosition) node.getPreviousSibling().getUserData(POS_KEY);
- }
- if (pos == null) {
- pos = (OffsetPosition) node.getParentNode().getUserData(POS_KEY);
- }
- if (pos != null) {
- // Attempt to point forward to the actual text node
- int startOffset = pos.getOffset();
- int endOffset = pos.next.getOffset();
- int line = pos.getLine();
- int column = pos.getColumn();
-
- // Find attribute in the text
- String contents = (String) node.getOwnerDocument().getUserData(CONTENT_KEY);
- if (contents == null || contents.length() < endOffset) {
- return null;
- }
-
- boolean inAttribute = false;
- for (int offset = startOffset; offset <= endOffset; offset++) {
- char c = contents.charAt(offset);
- if (c == '>' && !inAttribute) {
- // Found the end of the element open tag: this is where the
- // text begins.
-
- // Skip >
- offset++;
-
- // Skip text whitespace prefix, if the text node contains non-whitespace
- // characters
- String text = node.getNodeValue();
- int textIndex = 0;
- int textLength = text.length();
- int newLine = line;
- int newColumn = column;
- for (; textIndex < text.length(); textIndex++) {
- char t = text.charAt(textIndex);
- if (t == '\n') {
- newLine++;
- newColumn = 0;
- } else {
- newColumn++;
- }
- if (!Character.isWhitespace(t)) {
- break;
- }
- }
- if (textIndex == textLength) {
- textIndex = 0; // Whitespace node
- } else {
- line = newLine;
- column = newColumn;
- }
-
- OffsetPosition attributePosition = new OffsetPosition(line, column,
- offset);
- // Also set end range for retrieval in getLocation
- attributePosition.next = new OffsetPosition(line, column,
- offset + textLength);
- return attributePosition;
- } else if (c == '"') {
- inAttribute = !inAttribute;
- } else if (c == '\n') {
- line++;
- column = -1; // pre-subtract column added below
- }
- column++;
- }
-
- return pos;
- }
- }
-
- return (OffsetPosition) node.getUserData(POS_KEY);
- }
-
- @Override
- public Location getLocation(XmlContext context, Node node) {
- OffsetPosition pos = (OffsetPosition) getPositions(node);
- if (pos != null) {
- return Location.create(context.file, pos, pos.next);
- }
-
- return null;
- }
-
- @Override
- public Handle createLocationHandle(XmlContext context, Node node) {
- return new LocationHandle(context.file, node);
- }
-
- /**
- * SAX parser handler which incrementally builds up a DOM document as we go
- * along, and updates position information along the way. Position
- * information is attached to the DOM nodes by setting user data with the
- * {@link POS_KEY} key.
- */
- private static final class DomBuilder extends DefaultHandler {
- private final String mXml;
- private final Document mDocument;
- private Locator mLocator;
- private int mCurrentLine = 0;
- private int mCurrentOffset;
- private int mCurrentColumn;
- private final List<Element> mStack = new ArrayList<Element>();
- private final StringBuilder mPendingText = new StringBuilder();
-
- private DomBuilder(String xml) throws ParserConfigurationException {
- mXml = xml;
-
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
- factory.setValidating(false);
- DocumentBuilder docBuilder = factory.newDocumentBuilder();
- mDocument = docBuilder.newDocument();
- mDocument.setUserData(CONTENT_KEY, xml, null);
- }
-
- /** Returns the document parsed by the handler */
- Document getDocument() {
- return mDocument;
- }
-
- @Override
- public void setDocumentLocator(Locator locator) {
- this.mLocator = locator;
- }
-
- @Override
- public void startElement(String uri, String localName, String qName,
- Attributes attributes) throws SAXException {
- flushText();
- Element element = mDocument.createElement(qName);
- for (int i = 0; i < attributes.getLength(); i++) {
- if (attributes.getURI(i) != null && attributes.getURI(i).length() > 0) {
- Attr attr = mDocument.createAttributeNS(attributes.getURI(i),
- attributes.getQName(i));
- attr.setValue(attributes.getValue(i));
- element.setAttributeNodeNS(attr);
- assert attr.getOwnerElement() == element;
- } else {
- Attr attr = mDocument.createAttribute(attributes.getQName(i));
- attr.setValue(attributes.getValue(i));
- element.setAttributeNode(attr);
- assert attr.getOwnerElement() == element;
- }
- }
-
- OffsetPosition pos = getCurrentPosition();
-
- // The starting position reported to us by SAX is really the END of the
- // open tag in an element, when all the attributes have been processed.
- // We have to scan backwards to find the real beginning. We'll do that
- // by scanning backwards.
- // -1: Make sure that when we have <foo></foo> we don't consider </foo>
- // the beginning since pos.offset will typically point to the first character
- // AFTER the element open tag, which could be a closing tag or a child open
- // tag
-
- for (int offset = pos.getOffset() - 1; offset >= 0; offset--) {
- char c = mXml.charAt(offset);
- // < cannot appear in attribute values or anywhere else within
- // an element open tag, so we know the first occurrence is the real
- // element start
- if (c == '<') {
- // Adjust line position
- int line = pos.getLine();
- for (int i = offset, n = pos.getOffset(); i < n; i++) {
- if (mXml.charAt(i) == '\n') {
- line--;
- }
- }
-
- // Compute new column position
- int column = 0;
- for (int i = offset; i >= 0; i--, column++) {
- if (mXml.charAt(i) == '\n') {
- break;
- }
- }
-
- pos = new OffsetPosition(line, column, offset);
- break;
- }
- }
-
- element.setUserData(POS_KEY, pos, null);
- mStack.add(element);
- }
-
- @Override
- public void endElement(String uri, String localName, String qName) {
- flushText();
- Element element = mStack.remove(mStack.size() - 1);
-
- OffsetPosition pos = (OffsetPosition) element.getUserData(POS_KEY);
- assert pos != null;
- pos.next = getCurrentPosition();
-
- if (mStack.isEmpty()) {
- mDocument.appendChild(element);
- } else {
- Element parent = mStack.get(mStack.size() - 1);
- parent.appendChild(element);
- }
- }
-
- /**
- * Returns a position holder for the current position. The most
- * important part of this function is to incrementally compute the
- * offset as well, by counting forwards until it reaches the new line
- * number and column position of the XML parser, counting characters as
- * it goes along.
- */
- private OffsetPosition getCurrentPosition() {
- int line = mLocator.getLineNumber() - 1;
- int column = mLocator.getColumnNumber() - 1;
-
- // Compute offset incrementally now that we have the new line and column
- // numbers
- while (mCurrentLine < line) {
- char c = mXml.charAt(mCurrentOffset);
- if (c == '\r' && mCurrentOffset < mXml.length() - 1) {
- if (mXml.charAt(mCurrentOffset + 1) != '\n') {
- mCurrentLine++;
- mCurrentColumn = 0;
- }
- } else if (c == '\n') {
- mCurrentLine++;
- mCurrentColumn = 0;
- } else {
- mCurrentColumn++;
- }
- mCurrentOffset++;
- }
-
- mCurrentOffset += column - mCurrentColumn;
- mCurrentColumn = column;
-
- return new OffsetPosition(mCurrentLine, mCurrentColumn, mCurrentOffset);
- }
-
- @Override
- public void characters(char c[], int start, int length) throws SAXException {
- mPendingText.append(c, start, length);
- }
-
- private void flushText() {
- if (mPendingText.length() > 0 && !mStack.isEmpty()) {
- Element element = mStack.get(mStack.size() - 1);
- Node textNode = mDocument.createTextNode(mPendingText.toString());
- element.appendChild(textNode);
- mPendingText.setLength(0);
- }
- }
- }
-
- private static class OffsetPosition extends Position {
- /** The line number (0-based where the first line is line 0) */
- private final int mLine;
-
- /**
- * The column number (where the first character on the line is 0), or -1 if
- * unknown
- */
- private final int mColumn;
-
- /** The character offset */
- private final int mOffset;
-
- /**
- * Linked position: for a begin offset this will point to the end
- * offset, and for an end offset this will be null
- */
- public OffsetPosition next;
-
- /**
- * Creates a new {@link Position}
- *
- * @param line the 0-based line number, or -1 if unknown
- * @param column the 0-based column number, or -1 if unknown
- * @param offset the offset, or -1 if unknown
- */
- public OffsetPosition(int line, int column, int offset) {
- this.mLine = line;
- this.mColumn = column;
- this.mOffset = offset;
- }
-
- @Override
- public int getLine() {
- return mLine;
- }
-
- @Override
- public int getOffset() {
- return mOffset;
- }
-
- @Override
- public int getColumn() {
- return mColumn;
- }
- }
-
- @Override
- public void dispose(XmlContext context, Document document) {
- }
-
- /* Handle for creating DOM positions cheaply and returning full fledged locations later */
- private class LocationHandle implements Handle {
- private File mFile;
- private Node mNode;
-
- public LocationHandle(File file, Node node) {
- mFile = file;
- mNode = node;
- }
-
- @Override
- public Location resolve() {
- OffsetPosition pos = (OffsetPosition) getPositions(mNode);
- if (pos != null) {
- return Location.create(mFile, pos, pos.next);
- }
-
- return null;
- }
- }
-}
diff --git a/lint/libs/lint_checks/tests/.classpath b/lint/libs/lint_checks/tests/.classpath
index 73067c0..7e1e017 100644
--- a/lint/libs/lint_checks/tests/.classpath
+++ b/lint/libs/lint_checks/tests/.classpath
@@ -7,5 +7,7 @@
<classpathentry combineaccessrules="false" kind="src" path="/lint-api"/>
<classpathentry combineaccessrules="false" kind="src" path="/lint-checks"/>
<classpathentry combineaccessrules="false" kind="src" path="/lint-cli"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/common"/>
+ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/guava-tools/guava-10.0.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/guava-tools/src.zip"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/lint/libs/lint_checks/tests/Android.mk b/lint/libs/lint_checks/tests/Android.mk
index fda6d7f..a0d9bd0 100644
--- a/lint/libs/lint_checks/tests/Android.mk
+++ b/lint/libs/lint_checks/tests/Android.mk
@@ -22,7 +22,7 @@
LOCAL_MODULE := lint_checks-tests
LOCAL_MODULE_TAGS := optional
-LOCAL_JAVA_LIBRARIES := lint_api lint_checks lint junit easymock
+LOCAL_JAVA_LIBRARIES := common lint_api lint_checks lint junit easymock
include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/PositionXmlParserTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/LintCliXmlParserTest.java
similarity index 83%
rename from lint/libs/lint_checks/tests/src/com/android/tools/lint/PositionXmlParserTest.java
rename to lint/libs/lint_checks/tests/src/com/android/tools/lint/LintCliXmlParserTest.java
index 1c78c44..212b01e 100644
--- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/PositionXmlParserTest.java
+++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/LintCliXmlParserTest.java
@@ -16,6 +16,9 @@
package com.android.tools.lint;
+import com.android.tools.lint.client.api.LintClient;
+import com.android.tools.lint.detector.api.Context;
+import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Location.Handle;
import com.android.tools.lint.detector.api.Position;
@@ -37,7 +40,7 @@
import junit.framework.TestCase;
@SuppressWarnings("javadoc")
-public class PositionXmlParserTest extends TestCase {
+public class LintCliXmlParserTest extends TestCase {
public void test() throws Exception {
String xml =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
@@ -59,13 +62,15 @@
" android:text=\"Button\" />\n" +
"\n" +
"</LinearLayout>\n";
- PositionXmlParser parser = new PositionXmlParser();
+ LintCliXmlParser parser = new LintCliXmlParser();
File file = File.createTempFile("parsertest", ".xml");
Writer fw = new BufferedWriter(new FileWriter(file));
fw.write(xml);
fw.close();
- Project project = new Project(null, file.getParentFile(), file.getParentFile());
- XmlContext context = new XmlContext(new Main(), project, file,
+ LintClient client = new TestClient();
+ Project project = new Project(client, file.getParentFile(), file.getParentFile());
+ project.setConfiguration(client.getConfiguration(project));
+ XmlContext context = new XmlContext(client, project, file,
EnumSet.of(Scope.RESOURCE_FILE));
Document document = parser.parseXml(context);
assertNotNull(document);
@@ -132,17 +137,27 @@
"\r" +
"<LinearLayout></LinearLayout>\r\n" +
"</LinearLayout>\r\n";
- PositionXmlParser parser = new PositionXmlParser();
+ LintCliXmlParser parser = new LintCliXmlParser();
File file = File.createTempFile("parsertest2", ".xml");
Writer fw = new BufferedWriter(new FileWriter(file));
fw.write(xml);
fw.close();
- Project project = new Project(null, file.getParentFile(), file.getParentFile());
- XmlContext context = new XmlContext(new Main(), project, file,
+ LintClient client = new TestClient();
+ Project project = new Project(client, file.getParentFile(), file.getParentFile());
+ project.setConfiguration(client.getConfiguration(project));
+ XmlContext context = new XmlContext(client, project, file,
EnumSet.of(Scope.RESOURCE_FILE));
Document document = parser.parseXml(context);
assertNotNull(document);
file.delete();
}
+
+ private static class TestClient extends Main {
+ @Override
+ public void report(Context context, Issue issue, Location location, String message,
+ Object data) {
+ System.out.println(location + ":" + message);
+ }
+ }
}
diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/AbstractCheckTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/AbstractCheckTest.java
index 58f1045..dd18ffa 100644
--- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/AbstractCheckTest.java
+++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/AbstractCheckTest.java
@@ -16,12 +16,12 @@
package com.android.tools.lint.checks;
-import com.android.tools.lint.PositionXmlParser;
+import com.android.tools.lint.LintCliXmlParser;
+import com.android.tools.lint.Main;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.client.api.IDomParser;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.Lint;
-import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Issue;
@@ -33,7 +33,6 @@
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
-import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
@@ -253,7 +252,7 @@
return false;
}
- private class TestLintClient extends LintClient {
+ private class TestLintClient extends Main {
private List<String> mErrors = new ArrayList<String>();
public List<String> getErrors() {
@@ -344,17 +343,7 @@
@Override
public IDomParser getDomParser() {
- return new PositionXmlParser();
- }
-
- @Override
- public String readFile(File file) {
- try {
- return AbstractCheckTest.readFile(new FileReader(file));
- } catch (Throwable e) {
- fail(e.toString());
- }
- return null;
+ return new LintCliXmlParser();
}
@Override