/* * 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.utils; import com.android.ide.common.blame.SourcePosition; import junit.framework.TestCase; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; 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; @SuppressWarnings({"javadoc", "IOResourceOpenedButNotSafelyClosed"}) 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"; File file = File.createTempFile("parsertest", ".xml"); Writer fw = new BufferedWriter(new FileWriter(file)); fw.write(xml); fw.close(); Document document = PositionXmlParser.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); SourcePosition position = PositionXmlParser.getPosition(attr); assertEquals(2, position.getStartLine()); assertEquals(4, position.getStartColumn()); assertEquals(xml.indexOf("android:layout_width"), position.getStartOffset()); assertEquals(2, position.getEndLine()); String target = "android:layout_width=\"match_parent\""; assertEquals(xml.indexOf(target) + target.length(), position.getEndOffset()); // Check element positions Element button = (Element) buttons.item(0); position = PositionXmlParser.getPosition(button); assertEquals(6, position.getStartLine()); assertEquals(4, position.getStartColumn()); assertEquals(xml.indexOf("<Button"), position.getStartOffset()); assertEquals(xml.indexOf("/>") + 2, position.getEndOffset()); assertEquals(10, position.getEndLine()); int button1End = position.getEndOffset(); Element button2 = (Element) buttons.item(1); position = PositionXmlParser.getPosition(button2); assertEquals(12, position.getStartLine()); assertEquals(4, position.getStartColumn()); assertEquals(xml.indexOf("<Button", button1End), position.getStartOffset()); assertEquals(xml.indexOf("/>", position.getStartOffset()) + 2, position.getEndOffset()); assertEquals(16, position.getEndLine()); file.delete(); } public static void testText() 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" + " some text\n" + "\n" + "</LinearLayout>\n"; File file = File.createTempFile("parsertest", ".xml"); file.deleteOnExit(); Writer fw = new BufferedWriter(new FileWriter(file)); fw.write(xml); fw.close(); Document document = PositionXmlParser.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(1, buttons.getLength()); final String ANDROID_URI = "http://schemas.android.com/apk/res/android"; assertEquals("wrap_content", linearLayout.getAttributeNS(ANDROID_URI, "layout_height")); // Check text positions Element button = (Element) buttons.item(0); Text text = (Text) button.getNextSibling(); assertNotNull(text); // Check attribute positions SourcePosition start = PositionXmlParser.getPosition(text); assertEquals(11, start.getStartLine()); assertEquals(10, start.getStartColumn()); assertEquals(xml.indexOf("some text"), start.getStartOffset()); // Check attribute positions with text node offsets start = PositionXmlParser.getPosition(text, 13, 15); assertEquals(11, start.getStartLine()); assertEquals(12, start.getStartColumn()); assertEquals(xml.indexOf("me"), start.getStartOffset()); } 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"; File file = File.createTempFile("parsertest2", ".xml"); Writer fw = new BufferedWriter(new FileWriter(file)); fw.write(xml); fw.close(); Document document = PositionXmlParser.parse(new FileInputStream(file)); assertNotNull(document); file.delete(); } private static void checkEncoding(String encoding, boolean writeBom, boolean writeEncoding, String lineEnding) throws Exception { // Norwegian extra vowel characters such as "latin small letter a with ring above" String value = "\u00e6\u00d8\u00e5"; StringBuilder sb = new StringBuilder(); sb.append("<?xml version=\"1.0\""); if (writeEncoding) { sb.append(" encoding=\""); sb.append(encoding); sb.append("\""); } sb.append("?>"); sb.append(lineEnding); sb.append( "<!-- This is a " + lineEnding + " multiline comment" + lineEnding + "-->" + lineEnding + "<foo "); int startAttrOffset = sb.length(); sb.append("attr=\""); sb.append(value); sb.append("\""); sb.append(">" + lineEnding + lineEnding + "<bar></bar>" + lineEnding + "</foo>" + lineEnding); 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 = PositionXmlParser.parse(new FileInputStream(file)); assertNotNull(document); Element root = document.getDocumentElement(); assertEquals(file.getPath(), value, root.getAttribute("attr")); assertEquals(4, PositionXmlParser.getPosition(root).getStartLine()); Attr attribute = root.getAttributeNode("attr"); assertNotNull(attribute); SourcePosition position = PositionXmlParser.getPosition(attribute); assertNotNull(position); assertEquals(4, position.getStartLine()); assertEquals(startAttrOffset, position.getStartOffset()); file.delete(); } public void testEncoding() throws Exception { checkEncoding("utf-8", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF-8", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF_16", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF-16", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF_16LE", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF_32", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF_32LE", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("windows-1252", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("MacRoman", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("ISO-8859-1", false /*bom*/, true /*encoding*/, "\n"); checkEncoding("iso-8859-1", false /*bom*/, true /*encoding*/, "\n"); // Try BOM's (with no encoding specified) checkEncoding("utf-8", true /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF-8", true /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF_16", true /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF-16", true /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF_16LE", true /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF_32", true /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF_32LE", true /*bom*/, false /*encoding*/, "\n"); // Try default encodings (only defined for utf-8 and utf-16) checkEncoding("utf-8", false /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF-8", false /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF_16", false /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF-16", false /*bom*/, false /*encoding*/, "\n"); checkEncoding("UTF_16LE", false /*bom*/, false /*encoding*/, "\n"); // Try BOM's (with explicit encoding specified) checkEncoding("utf-8", true /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF-8", true /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF_16", true /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF-16", true /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF_16LE", true /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF_32", true /*bom*/, true /*encoding*/, "\n"); checkEncoding("UTF_32LE", true /*bom*/, true /*encoding*/, "\n"); // Make sure this works for \r and \r\n as well checkEncoding("UTF-16", false /*bom*/, true /*encoding*/, "\r"); checkEncoding("UTF_16LE", false /*bom*/, true /*encoding*/, "\r"); checkEncoding("UTF_32", false /*bom*/, true /*encoding*/, "\r"); checkEncoding("UTF_32LE", false /*bom*/, true /*encoding*/, "\r"); checkEncoding("windows-1252", false /*bom*/, true /*encoding*/, "\r"); checkEncoding("MacRoman", false /*bom*/, true /*encoding*/, "\r"); checkEncoding("ISO-8859-1", false /*bom*/, true /*encoding*/, "\r"); checkEncoding("iso-8859-1", false /*bom*/, true /*encoding*/, "\r"); checkEncoding("UTF-16", false /*bom*/, true /*encoding*/, "\r\n"); checkEncoding("UTF_16LE", false /*bom*/, true /*encoding*/, "\r\n"); checkEncoding("UTF_32", false /*bom*/, true /*encoding*/, "\r\n"); checkEncoding("UTF_32LE", false /*bom*/, true /*encoding*/, "\r\n"); checkEncoding("windows-1252", false /*bom*/, true /*encoding*/, "\r\n"); checkEncoding("MacRoman", false /*bom*/, true /*encoding*/, "\r\n"); checkEncoding("ISO-8859-1", false /*bom*/, true /*encoding*/, "\r\n"); checkEncoding("iso-8859-1", false /*bom*/, true /*encoding*/, "\r\n"); } public void testOneLineComment() 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" + " <!-- this is a comment ! -->\n" + " <Button\n" + " android:id=\"@+id/button1\"\n" + " android:layout_width=\"wrap_content\"\n" + " android:layout_height=\"wrap_content\"\n" + " android:text=\"Button\" />\n" + " some text\n" + "\n" + "</LinearLayout>\n"; File file = File.createTempFile("parsertest", ".xml"); file.deleteOnExit(); Writer fw = new BufferedWriter(new FileWriter(file)); fw.write(xml); fw.close(); Document document = PositionXmlParser.parse(new FileInputStream(file)); assertNotNull(document); // Basic parsing heart beat tests Element linearLayout = (Element) document.getElementsByTagName("LinearLayout").item(0); assertNotNull(linearLayout); // first child is a comment. org.w3c.dom.Node commentNode = linearLayout.getFirstChild().getNextSibling(); assertEquals(Node.COMMENT_NODE, commentNode.getNodeType()); SourcePosition position = PositionXmlParser.getPosition(commentNode); assertNotNull(position); assertEquals(6, position.getStartLine()); assertEquals(4, position.getStartColumn()); assertEquals(xml.indexOf("<!--"), position.getStartOffset()); // ensure that the next siblings' position start at the right location. Element button = (Element) document.getElementsByTagName("Button").item(0); SourcePosition buttonPosition = PositionXmlParser.getPosition(button); assertNotNull(buttonPosition); assertEquals(7, buttonPosition.getStartLine()); assertEquals(4, buttonPosition.getStartColumn()); } public void testMultipleLineComment() 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" + " <!-- this is a more complicated \n" + " which spans on multiple lines\n" + " in the source xml -->\n" + " <Button\n" + " android:id=\"@+id/button1\"\n" + " android:layout_width=\"wrap_content\"\n" + " android:layout_height=\"wrap_content\"\n" + " android:text=\"Button\" />\n" + " some text\n" + "\n" + "</LinearLayout>\n"; File file = File.createTempFile("parsertest", ".xml"); file.deleteOnExit(); Writer fw = new BufferedWriter(new FileWriter(file)); fw.write(xml); fw.close(); Document document = PositionXmlParser.parse(new FileInputStream(file)); assertNotNull(document); // Basic parsing heart beat tests Element linearLayout = (Element) document.getElementsByTagName("LinearLayout").item(0); assertNotNull(linearLayout); // first child is a comment. Node commentNode = linearLayout.getFirstChild().getNextSibling(); assertEquals(Node.COMMENT_NODE, commentNode.getNodeType()); SourcePosition position = PositionXmlParser.getPosition(commentNode); assertNotNull(position); assertEquals(6, position.getStartLine()); assertEquals(4, position.getStartColumn()); // ensure that the next siblings' position start at the right location. Element button = (Element) document.getElementsByTagName("Button").item(0); SourcePosition buttonPosition = PositionXmlParser.getPosition(button); assertNotNull(buttonPosition); assertEquals(9, buttonPosition.getStartLine()); assertEquals(4, buttonPosition.getStartColumn()); } public void testAttributeWithoutNamespace() throws Exception { // Search for attribute with different prefixes and with no prefix. // Make sure we find it regardless of which one comes first (which is why we // list all 3 orders here and search for all attributes in all 3.) String xml = "" + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<LinearLayout\n" + " xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + " xmlns:other=\"http://foo.bar\">\n" + " <Button\n" + " android:id=\"@+id/button1\"\n" + " android:orientation=\"vertical\"\n" + " other:orientation=\"vertical\"\n" + " orientation=\"true\"/>\n" + " <Button\n" + " android:id=\"@+id/button2\"\n" + " other:orientation=\"vertical\"\n" + " android:orientation=\"vertical\"\n" + " orientation=\"true\"/>\n" + " <Button\n" + " android:id=\"@+id/button3\"\n" + " orientation=\"true\"\n" + " android:orientation=\"vertical\"\n" + " other:orientation=\"vertical\"/>\n" + "</LinearLayout>\n" + "\n"; File file = File.createTempFile("parsertest", ".xml"); file.deleteOnExit(); Writer fw = new BufferedWriter(new FileWriter(file)); fw.write(xml); fw.close(); Document document = PositionXmlParser.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(3, buttons.getLength()); // Check attribute positions for (int i = 0, n = buttons.getLength(); i < n; i++) { Element button = (Element)buttons.item(i); Attr attr; SourcePosition position; attr = button.getAttributeNode("orientation"); position = PositionXmlParser.getPosition(attr); assertNotNull(position); assertEquals(" orientation=\"true\"", xml.substring(position.getStartOffset() - 1, position.getEndOffset())); attr = button.getAttributeNodeNS("http://schemas.android.com/apk/res/android", "orientation"); position = PositionXmlParser.getPosition(attr); assertNotNull(position); assertEquals("android:orientation=\"vertical\"", xml.substring(position.getStartOffset(), position.getEndOffset())); attr = button.getAttributeNodeNS("http://foo.bar", "orientation"); position = PositionXmlParser.getPosition(attr); assertNotNull(position); assertEquals("other:orientation=\"vertical\"", xml.substring(position.getStartOffset(), position.getEndOffset())); } } public void testTagNamespace() throws Exception { final String NAMESPACE_URL = "http://example.org/path"; final String XML = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<ns:SomeTag xmlns:ns=\"" + NAMESPACE_URL + "\">\n" + " <ns:SubTag\n" + " ns:text=\"Button\" />\n" + "</ns:SomeTag>\n"; File file = File.createTempFile("parsertest", ".xml"); Writer fw = new BufferedWriter(new FileWriter(file)); fw.write(XML); fw.close(); Document document = PositionXmlParser.parse(new FileInputStream(file)); assertNotNull(document); Element e = document.getDocumentElement(); assertNotNull(e); assertEquals("http://example.org/path", e.getNamespaceURI()); assertEquals("ns:SomeTag", e.getTagName()); assertEquals("ns:SomeTag", e.getNodeName()); assertEquals("SomeTag",e.getLocalName()); NodeList subTags = e.getElementsByTagNameNS(NAMESPACE_URL, "SubTag"); assertEquals(1, subTags.getLength()); Node subTagNode = subTags.item(0); assertEquals(Node.ELEMENT_NODE, subTagNode.getNodeType()); Element subTag = (Element) subTagNode; assertEquals("ns:SubTag", subTag.getTagName()); assertEquals("ns:SubTag", subTag.getNodeName()); assertEquals("SubTag", subTag.getLocalName()); Attr attr = subTag.getAttributeNodeNS(NAMESPACE_URL, "text"); assertNotNull(attr); SourcePosition position = PositionXmlParser.getPosition(attr); assertNotNull(position); assertEquals("ns:text=\"Button\"", XML.substring(position.getStartOffset(), position.getEndOffset())); assertEquals("Button", subTag.getAttributeNS(NAMESPACE_URL, "text")); assertEquals(NAMESPACE_URL, subTag.getNamespaceURI()); } }