aboutsummaryrefslogtreecommitdiffstats
path: root/common
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2012-01-06 11:09:55 -0800
committerAndroid (Google) Code Review <android-gerrit@google.com>2012-01-06 11:09:55 -0800
commit8094b1454d617ad231ac9ba77b0b8b9770245102 (patch)
treedf1814131d751fd185a413952f9845a21f23938b /common
parent9f338c0725644f84597366c1e4acb680a72b3f00 (diff)
parent581ca2e75fd638f6e95570ac54ff9e6b7b7cde84 (diff)
downloadsdk-8094b1454d617ad231ac9ba77b0b8b9770245102.zip
sdk-8094b1454d617ad231ac9ba77b0b8b9770245102.tar.gz
sdk-8094b1454d617ad231ac9ba77b0b8b9770245102.tar.bz2
Merge "Extract positional XML parser into common and fix encoding issues"
Diffstat (limited to 'common')
-rw-r--r--common/.settings/org.eclipse.core.resources.prefs3
-rw-r--r--common/.settings/org.eclipse.jdt.core.prefs93
-rw-r--r--common/.settings/org.moreunit.prefs5
-rw-r--r--common/src/com/android/io/FileWrapper.java7
-rw-r--r--common/src/com/android/io/FolderWrapper.java8
-rw-r--r--common/src/com/android/resources/Density.java5
-rw-r--r--common/src/com/android/resources/Keyboard.java5
-rw-r--r--common/src/com/android/resources/KeyboardState.java5
-rw-r--r--common/src/com/android/resources/Navigation.java5
-rw-r--r--common/src/com/android/resources/NavigationState.java5
-rw-r--r--common/src/com/android/resources/NightMode.java5
-rw-r--r--common/src/com/android/resources/ScreenOrientation.java5
-rw-r--r--common/src/com/android/resources/ScreenRatio.java5
-rw-r--r--common/src/com/android/resources/ScreenSize.java5
-rw-r--r--common/src/com/android/resources/TouchScreen.java5
-rw-r--r--common/src/com/android/resources/UiMode.java5
-rw-r--r--common/src/com/android/util/PositionXmlParser.java675
-rw-r--r--common/tests/.classpath1
-rw-r--r--common/tests/src/com/android/util/PositionXmlParserTest.java243
19 files changed, 1090 insertions, 0 deletions
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 @@ public class FileWrapper extends File implements IAbstractFile {
super(uri);
}
+ @Override
public InputStream getContents() throws StreamException {
try {
return new FileInputStream(this);
@@ -93,6 +94,7 @@ public class FileWrapper extends File implements IAbstractFile {
}
}
+ @Override
public void setContents(InputStream source) throws StreamException {
FileOutputStream fos = null;
try {
@@ -116,6 +118,7 @@ public class FileWrapper extends File implements IAbstractFile {
}
}
+ @Override
public OutputStream getOutputStream() throws StreamException {
try {
return new FileOutputStream(this);
@@ -124,10 +127,12 @@ public class FileWrapper extends File implements IAbstractFile {
}
}
+ @Override
public PreferredWriteMode getPreferredWriteMode() {
return PreferredWriteMode.OUTPUTSTREAM;
}
+ @Override
public String getOsLocation() {
return getAbsolutePath();
}
@@ -137,10 +142,12 @@ public class FileWrapper extends File implements IAbstractFile {
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 @@ public class FolderWrapper extends File implements IAbstractFolder {
super(file.getAbsolutePath());
}
+ @Override
public IAbstractResource[] listMembers() {
File[] files = listFiles();
final int count = files == null ? 0 : files.length;
@@ -100,8 +101,10 @@ public class FolderWrapper extends File implements IAbstractFolder {
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 @@ public class FolderWrapper extends File implements IAbstractFolder {
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 @@ public class FolderWrapper extends File implements IAbstractFolder {
return new FolderWrapper(p);
}
+ @Override
public String getOsLocation() {
return getAbsolutePath();
}
@@ -135,6 +142,7 @@ public class FolderWrapper extends File implements IAbstractFolder {
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 @@ public enum Density implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
@@ -88,10 +89,12 @@ public enum Density implements ResourceEnum {
return "";
}
+ @Override
public String getShortDisplayValue() {
return mDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mDisplayValue;
}
@@ -120,10 +123,12 @@ public enum Density implements ResourceEnum {
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 @@ public enum Keyboard implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -89,10 +92,12 @@ public enum Keyboard implements ResourceEnum {
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 @@ public enum KeyboardState implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -86,10 +89,12 @@ public enum KeyboardState implements ResourceEnum {
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 @@ public enum Navigation implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -87,10 +90,12 @@ public enum Navigation implements ResourceEnum {
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 @@ public enum NavigationState implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -85,10 +88,12 @@ public enum NavigationState implements ResourceEnum {
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 @@ public enum NightMode implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -85,10 +88,12 @@ public enum NightMode implements ResourceEnum {
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 @@ public enum ScreenOrientation implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -87,10 +90,12 @@ public enum ScreenOrientation implements ResourceEnum {
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 @@ public enum ScreenRatio implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -86,10 +89,12 @@ public enum ScreenRatio implements ResourceEnum {
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 @@ public enum ScreenSize implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -88,10 +91,12 @@ public enum ScreenSize implements ResourceEnum {
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 @@ public enum TouchScreen implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mShortDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mLongDisplayValue;
}
@@ -87,10 +90,12 @@ public enum TouchScreen implements ResourceEnum {
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 @@ public enum UiMode implements ResourceEnum {
return null;
}
+ @Override
public String getResourceValue() {
return mValue;
}
+ @Override
public String getShortDisplayValue() {
return mDisplayValue;
}
+ @Override
public String getLongDisplayValue() {
return mDisplayValue;
}
@@ -85,10 +88,12 @@ public enum UiMode implements ResourceEnum {
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*/);
+ }
+}