Merge "Implementing getBaseUri() for DOM."
diff --git a/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java b/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
index 5940417..2a8e1fa 100644
--- a/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
+++ b/xml/src/main/java/org/apache/harmony/xml/dom/NodeImpl.java
@@ -16,6 +16,8 @@
 
 package org.apache.harmony.xml.dom;
 
+import org.apache.xml.serializer.utils.SystemIDResolver;
+import org.apache.xml.utils.URI;
 import org.w3c.dom.Attr;
 import org.w3c.dom.CharacterData;
 import org.w3c.dom.DOMException;
@@ -27,6 +29,7 @@
 import org.w3c.dom.ProcessingInstruction;
 import org.w3c.dom.UserDataHandler;
 
+import javax.xml.transform.TransformerException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -244,35 +247,83 @@
         return matchesName(namespaceURI, getNamespaceURI(), wildcard) && matchesName(localName, getLocalName(), wildcard);
     }
 
-    public String getBaseURI() {
-        /*
-         * TODO: implement. For reference, here's Xerces' behaviour:
-         *
-         * In all cases, the returned URI should be sanitized before it is
-         * returned. If the URI is malformed, null should be returned instead.
-         *
-         * For document nodes, this should return a member field that's
-         * initialized by the parser.
-         *
-         * For element nodes, this should first look for the xml:base attribute.
-         *   if that exists and is absolute, it should be returned.
-         *   if that exists and is relative, it should be resolved to the parent's base URI
-         *   if it doesn't exist, the parent's baseURI should be returned
-         *
-         * For entity nodes, if a base URI exists that should be returned.
-         * Otherwise the document's base URI should be returned
-         *
-         * For entity references, if a base URI exists that should be returned
-         * otherwise it dereferences the entity (via the document) and uses the
-         * entity's base URI.
-         *
-         * For notations, it returns the base URI field.
-         *
-         * For processing instructions, it returns the parent's base URI.
-         *
-         * For all other node types, it returns null.
-         */
-        return null;
+    public final String getBaseURI() {
+        switch (getNodeType()) {
+            case DOCUMENT_NODE:
+                return sanitizeUri(((Document) this).getDocumentURI());
+
+            case ELEMENT_NODE:
+                Element element = (Element) this;
+                String uri = element.getAttributeNS(
+                        "http://www.w3.org/XML/1998/namespace", "base"); // or "xml:base"
+
+                // if this node has no base URI, return the parent's.
+                if (uri == null || uri.length() == 0) {
+                    return getParentBaseUri();
+                }
+
+                // if this node's URI is absolute, return that
+                if (SystemIDResolver.isAbsoluteURI(uri)) {
+                    return uri;
+                }
+
+                // this node has a relative URI. Try to resolve it against the
+                // parent, but if that doesn't work just give up and return null.
+                String parentUri = getParentBaseUri();
+                if (parentUri == null) {
+                    return null;
+                }
+                try {
+                    return SystemIDResolver.getAbsoluteURI(uri, parentUri);
+                } catch (TransformerException e) {
+                    return null; // the spec requires that we swallow exceptions
+                }
+
+            case PROCESSING_INSTRUCTION_NODE:
+                return getParentBaseUri();
+
+            case NOTATION_NODE:
+            case ENTITY_NODE:
+                // When we support these node types, the parser should
+                // initialize a base URI field on these nodes.
+                return null;
+
+            case ENTITY_REFERENCE_NODE:
+                // TODO: get this value from the parser, falling back to the
+                // referenced entity's baseURI if that doesn't exist
+                return null;
+
+            case DOCUMENT_TYPE_NODE:
+            case DOCUMENT_FRAGMENT_NODE:
+            case ATTRIBUTE_NODE:
+            case TEXT_NODE:
+            case CDATA_SECTION_NODE:
+            case COMMENT_NODE:
+                return null;
+
+            default:
+                throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
+                        "Unsupported node type " + getNodeType());
+        }
+    }
+
+    private String getParentBaseUri() {
+        Node parentNode = getParentNode();
+        return parentNode != null ? parentNode.getBaseURI() : null;
+    }
+
+    /**
+     * Returns the sanitized input if it is a URI, or {@code null} otherwise.
+     */
+    private String sanitizeUri(String uri) {
+        if (uri == null || uri.length() == 0) {
+            return null;
+        }
+        try {
+            return new URI(uri).toString();
+        } catch (URI.MalformedURIException e) {
+            return null;
+        }
     }
 
     public short compareDocumentPosition(Node other)
diff --git a/xml/src/test/java/tests/xml/DomTest.java b/xml/src/test/java/tests/xml/DomTest.java
index a5fdbd8..0f67c64 100644
--- a/xml/src/test/java/tests/xml/DomTest.java
+++ b/xml/src/test/java/tests/xml/DomTest.java
@@ -45,6 +45,8 @@
 import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
+import java.io.File;
+import java.io.FileWriter;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.util.ArrayList;
@@ -1006,6 +1008,136 @@
         assertEquals(expected, handler.calls);
     }
 
+    public void testBaseUriRelativeUriResolution() throws Exception {
+        File file = File.createTempFile("DomTest.java", "xml");
+        File parentFile = file.getParentFile();
+        FileWriter writer = new FileWriter(file);
+        writer.write("<a>"
+                + "  <b xml:base=\"b1/b2\">"
+                + "    <c>"
+                + "      <d xml:base=\"../d1/d2\"><e/></d>"
+                + "    </c>"
+                + "  </b>"
+                + "  <h xml:base=\"h1/h2/\">"
+                + "    <i xml:base=\"../i1/i2\"/>"
+                + "  </h>"
+                + "</a>");
+        writer.close();
+        document = builder.parse(file);
+
+        assertFileUriEquals("", file.getPath(), document.getBaseURI());
+        assertFileUriEquals("", file.getPath(), document.getDocumentURI());
+        Element a = document.getDocumentElement();
+        assertFileUriEquals("", file.getPath(), a.getBaseURI());
+
+        String message = "This implementation's getBaseURI() doesn't handle relative URIs";
+        Element b = (Element) a.getChildNodes().item(1);
+        Element c = (Element) b.getChildNodes().item(1);
+        Element d = (Element) c.getChildNodes().item(1);
+        Element e = (Element) d.getChildNodes().item(0);
+        Element h = (Element) a.getChildNodes().item(3);
+        Element i = (Element) h.getChildNodes().item(1);
+        assertFileUriEquals(message, parentFile + "/b1/b2", b.getBaseURI());
+        assertFileUriEquals(message, parentFile + "/b1/b2", c.getBaseURI());
+        assertFileUriEquals(message, parentFile + "/d1/d2", d.getBaseURI());
+        assertFileUriEquals(message, parentFile + "/d1/d2", e.getBaseURI());
+        assertFileUriEquals(message, parentFile + "/h1/h2/", h.getBaseURI());
+        assertFileUriEquals(message, parentFile + "/h1/i1/i2", i.getBaseURI());
+    }
+
+    /**
+     * Regrettably both "file:/tmp/foo.txt" and "file:///tmp/foo.txt" are
+     * legal URIs, and different implementations emit different forms.
+     */
+    private void assertFileUriEquals(
+            String message, String expectedFile, String actual) {
+        if (!("file:" + expectedFile).equals(actual)
+                && !("file://" + expectedFile).equals(actual)) {
+            fail("Expected URI for: " + expectedFile
+                    + " but was " + actual + ". " + message);
+        }
+    }
+
+    /**
+     * According to the <a href="http://www.w3.org/TR/xmlbase/">XML Base</a>
+     * spec, fragments (like "#frag" or "") should not be dereferenced.
+     */
+    public void testBaseUriResolutionWithHashes() throws Exception {
+        document = builder.parse(new InputSource(new StringReader(
+                "<a xml:base=\"http://a1/a2\">"
+                        + "  <b xml:base=\"b1#b2\"/>"
+                        + "  <c xml:base=\"#c1\">"
+                        + "    <d xml:base=\"\"/>"
+                        + "  </c>"
+                        + "  <e xml:base=\"\"/>"
+                        + "</a>")));
+        Element a = document.getDocumentElement();
+        assertEquals("http://a1/a2", a.getBaseURI());
+
+        String message = "This implementation's getBaseURI() doesn't handle "
+                + "relative URIs with hashes";
+        Element b = (Element) a.getChildNodes().item(1);
+        Element c = (Element) a.getChildNodes().item(3);
+        Element d = (Element) c.getChildNodes().item(1);
+        Element e = (Element) a.getChildNodes().item(5);
+        assertEquals(message, "http://a1/b1#b2", b.getBaseURI());
+        assertEquals(message, "http://a1/a2#c1", c.getBaseURI());
+        assertEquals(message, "http://a1/a2#c1", d.getBaseURI());
+        assertEquals(message, "http://a1/a2", e.getBaseURI());
+    }
+
+    public void testBaseUriInheritedForProcessingInstructions() {
+        document.setDocumentURI("http://d1/d2");
+        assertEquals("http://d1/d2", wafflemaker.getBaseURI());
+    }
+
+    public void testBaseUriInheritedForEntities() {
+        if (sp == null) {
+            return;
+        }
+        document.setDocumentURI("http://d1/d2");
+        assertEquals("http://d1/d2", sp.getBaseURI());
+    }
+
+    public void testBaseUriNotInheritedForNotations() {
+        if (png == null) {
+            return;
+        }
+        document.setDocumentURI("http://d1/d2");
+        assertNull(png.getBaseURI());
+    }
+
+    public void testBaseUriNotInheritedForDoctypes() {
+        document.setDocumentURI("http://d1/d2");
+        assertNull(doctype.getBaseURI());
+    }
+
+    public void testBaseUriNotInheritedForAttributes() {
+        document.setDocumentURI("http://d1/d2");
+        assertNull(itemXmlns.getBaseURI());
+        assertNull(itemXmlnsA.getBaseURI());
+        assertNull(standard.getBaseURI());
+        assertNull(vitaminsXmlnsA.getBaseURI());
+    }
+
+    public void testBaseUriNotInheritedForTextsOrCdatas() {
+        document.setDocumentURI("http://d1/d2");
+        assertNull(descriptionText1.getBaseURI());
+        assertNull(descriptionText2.getBaseURI());
+        assertNull(option2Reference.getBaseURI());
+    }
+
+    public void testBaseUriNotInheritedForComments() {
+        document.setDocumentURI("http://d1/d2");
+        assertNull(descriptionText1.getBaseURI());
+        assertNull(descriptionText2.getBaseURI());
+    }
+
+    public void testBaseUriNotInheritedForEntityReferences() {
+        document.setDocumentURI("http://d1/d2");
+        assertNull(option2Reference.getBaseURI());
+    }
+
     private class RecordingHandler implements UserDataHandler {
         final Set<String> calls = new HashSet<String>();
         public void handle(short operation, String key, Object data, Node src, Node dst) {