1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.apache.harmony.xml.dom; 18 19 import java.net.URI; 20 import java.net.URISyntaxException; 21 import java.util.ArrayList; 22 import java.util.List; 23 import java.util.Map; 24 import org.w3c.dom.Attr; 25 import org.w3c.dom.CharacterData; 26 import org.w3c.dom.DOMException; 27 import org.w3c.dom.Document; 28 import org.w3c.dom.Element; 29 import org.w3c.dom.NamedNodeMap; 30 import org.w3c.dom.Node; 31 import org.w3c.dom.NodeList; 32 import org.w3c.dom.ProcessingInstruction; 33 import org.w3c.dom.TypeInfo; 34 import org.w3c.dom.UserDataHandler; 35 36 /** 37 * A straightforward implementation of the corresponding W3C DOM node. 38 * 39 * <p>Some fields have package visibility so other classes can access them while 40 * maintaining the DOM structure. 41 * 42 * <p>This class represents a Node that has neither a parent nor children. 43 * Subclasses may have either. 44 * 45 * <p>Some code was adapted from Apache Xerces. 46 */ 47 public abstract class NodeImpl implements Node { 48 49 private static final NodeList EMPTY_LIST = new NodeListImpl(); 50 51 static final TypeInfo NULL_TYPE_INFO = new TypeInfo() { 52 public String getTypeName() { 53 return null; 54 } 55 public String getTypeNamespace() { 56 return null; 57 } 58 public boolean isDerivedFrom( 59 String typeNamespaceArg, String typeNameArg, int derivationMethod) { 60 return false; 61 } 62 }; 63 64 /** 65 * The containing document. This is non-null except for DocumentTypeImpl 66 * nodes created by the DOMImplementation. 67 */ 68 DocumentImpl document; 69 70 NodeImpl(DocumentImpl document) { 71 this.document = document; 72 } 73 74 public Node appendChild(Node newChild) throws DOMException { 75 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null); 76 } 77 78 public final Node cloneNode(boolean deep) { 79 return document.cloneOrImportNode(UserDataHandler.NODE_CLONED, this, deep); 80 } 81 82 public NamedNodeMap getAttributes() { 83 return null; 84 } 85 86 public NodeList getChildNodes() { 87 return EMPTY_LIST; 88 } 89 90 public Node getFirstChild() { 91 return null; 92 } 93 94 public Node getLastChild() { 95 return null; 96 } 97 98 public String getLocalName() { 99 return null; 100 } 101 102 public String getNamespaceURI() { 103 return null; 104 } 105 106 public Node getNextSibling() { 107 return null; 108 } 109 110 public String getNodeName() { 111 return null; 112 } 113 114 public abstract short getNodeType(); 115 116 public String getNodeValue() throws DOMException { 117 return null; 118 } 119 120 public final Document getOwnerDocument() { 121 return document == this ? null : document; 122 } 123 124 public Node getParentNode() { 125 return null; 126 } 127 128 public String getPrefix() { 129 return null; 130 } 131 132 public Node getPreviousSibling() { 133 return null; 134 } 135 136 public boolean hasAttributes() { 137 return false; 138 } 139 140 public boolean hasChildNodes() { 141 return false; 142 } 143 144 public Node insertBefore(Node newChild, Node refChild) throws DOMException { 145 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null); 146 } 147 148 public boolean isSupported(String feature, String version) { 149 return DOMImplementationImpl.getInstance().hasFeature(feature, version); 150 } 151 152 public void normalize() { 153 } 154 155 public Node removeChild(Node oldChild) throws DOMException { 156 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null); 157 } 158 159 public Node replaceChild(Node newChild, Node oldChild) throws DOMException { 160 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null); 161 } 162 163 public final void setNodeValue(String nodeValue) throws DOMException { 164 switch (getNodeType()) { 165 case CDATA_SECTION_NODE: 166 case COMMENT_NODE: 167 case TEXT_NODE: 168 ((CharacterData) this).setData(nodeValue); 169 return; 170 171 case PROCESSING_INSTRUCTION_NODE: 172 ((ProcessingInstruction) this).setData(nodeValue); 173 return; 174 175 case ATTRIBUTE_NODE: 176 ((Attr) this).setValue(nodeValue); 177 return; 178 179 case ELEMENT_NODE: 180 case ENTITY_REFERENCE_NODE: 181 case ENTITY_NODE: 182 case DOCUMENT_NODE: 183 case DOCUMENT_TYPE_NODE: 184 case DOCUMENT_FRAGMENT_NODE: 185 case NOTATION_NODE: 186 return; // do nothing! 187 188 default: 189 throw new DOMException(DOMException.NOT_SUPPORTED_ERR, 190 "Unsupported node type " + getNodeType()); 191 } 192 } 193 194 public void setPrefix(String prefix) throws DOMException { 195 } 196 197 /** 198 * Validates the element or attribute namespace prefix on this node. 199 * 200 * @param namespaceAware whether this node is namespace aware 201 * @param namespaceURI this node's namespace URI 202 */ 203 static String validatePrefix(String prefix, boolean namespaceAware, String namespaceURI) { 204 if (!namespaceAware) { 205 throw new DOMException(DOMException.NAMESPACE_ERR, prefix); 206 } 207 208 if (prefix != null) { 209 if (namespaceURI == null 210 || !DocumentImpl.isXMLIdentifier(prefix) 211 || "xml".equals(prefix) && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI) 212 || "xmlns".equals(prefix) && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) { 213 throw new DOMException(DOMException.NAMESPACE_ERR, prefix); 214 } 215 } 216 217 return prefix; 218 } 219 220 /** 221 * Sets {@code node} to be namespace-aware and assigns its namespace URI 222 * and qualified name. 223 * 224 * @param node an element or attribute node. 225 * @param namespaceURI this node's namespace URI. May be null. 226 * @param qualifiedName a possibly-prefixed name like "img" or "html:img". 227 */ 228 static void setNameNS(NodeImpl node, String namespaceURI, String qualifiedName) { 229 if (qualifiedName == null) { 230 throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName); 231 } 232 233 String prefix = null; 234 int p = qualifiedName.lastIndexOf(":"); 235 if (p != -1) { 236 prefix = validatePrefix(qualifiedName.substring(0, p), true, namespaceURI); 237 qualifiedName = qualifiedName.substring(p + 1); 238 } 239 240 if (!DocumentImpl.isXMLIdentifier(qualifiedName)) { 241 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, qualifiedName); 242 } 243 244 switch (node.getNodeType()) { 245 case ATTRIBUTE_NODE: 246 if ("xmlns".equals(qualifiedName) 247 && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) { 248 throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName); 249 } 250 251 AttrImpl attr = (AttrImpl) node; 252 attr.namespaceAware = true; 253 attr.namespaceURI = namespaceURI; 254 attr.prefix = prefix; 255 attr.localName = qualifiedName; 256 break; 257 258 case ELEMENT_NODE: 259 ElementImpl element = (ElementImpl) node; 260 element.namespaceAware = true; 261 element.namespaceURI = namespaceURI; 262 element.prefix = prefix; 263 element.localName = qualifiedName; 264 break; 265 266 default: 267 throw new DOMException(DOMException.NOT_SUPPORTED_ERR, 268 "Cannot rename nodes of type " + node.getNodeType()); 269 } 270 } 271 272 /** 273 * Sets {@code node} to be not namespace-aware and assigns its name. 274 * 275 * @param node an element or attribute node. 276 */ 277 static void setName(NodeImpl node, String name) { 278 int prefixSeparator = name.lastIndexOf(":"); 279 if (prefixSeparator != -1) { 280 String prefix = name.substring(0, prefixSeparator); 281 String localName = name.substring(prefixSeparator + 1); 282 if (!DocumentImpl.isXMLIdentifier(prefix) || !DocumentImpl.isXMLIdentifier(localName)) { 283 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name); 284 } 285 } else if (!DocumentImpl.isXMLIdentifier(name)) { 286 throw new DOMException(DOMException.INVALID_CHARACTER_ERR, name); 287 } 288 289 switch (node.getNodeType()) { 290 case ATTRIBUTE_NODE: 291 AttrImpl attr = (AttrImpl) node; 292 attr.namespaceAware = false; 293 attr.localName = name; 294 break; 295 296 case ELEMENT_NODE: 297 ElementImpl element = (ElementImpl) node; 298 element.namespaceAware = false; 299 element.localName = name; 300 break; 301 302 default: 303 throw new DOMException(DOMException.NOT_SUPPORTED_ERR, 304 "Cannot rename nodes of type " + node.getNodeType()); 305 } 306 } 307 308 public final String getBaseURI() { 309 switch (getNodeType()) { 310 case DOCUMENT_NODE: 311 return sanitizeUri(((Document) this).getDocumentURI()); 312 313 case ELEMENT_NODE: 314 Element element = (Element) this; 315 String uri = element.getAttributeNS( 316 "http://www.w3.org/XML/1998/namespace", "base"); // or "xml:base" 317 318 try { 319 // if this node has no base URI, return the parent's. 320 if (uri == null || uri.isEmpty()) { 321 return getParentBaseUri(); 322 } 323 324 // if this node's URI is absolute, return it 325 if (new URI(uri).isAbsolute()) { 326 return uri; 327 } 328 329 // this node has a relative URI. Try to resolve it against the 330 // parent, but if that doesn't work just give up and return null. 331 String parentUri = getParentBaseUri(); 332 if (parentUri == null) { 333 return null; 334 } 335 336 return new URI(parentUri).resolve(uri).toString(); 337 } catch (URISyntaxException e) { 338 return null; 339 } 340 341 case PROCESSING_INSTRUCTION_NODE: 342 return getParentBaseUri(); 343 344 case NOTATION_NODE: 345 case ENTITY_NODE: 346 // When we support these node types, the parser should 347 // initialize a base URI field on these nodes. 348 return null; 349 350 case ENTITY_REFERENCE_NODE: 351 // TODO: get this value from the parser, falling back to the 352 // referenced entity's baseURI if that doesn't exist 353 return null; 354 355 case DOCUMENT_TYPE_NODE: 356 case DOCUMENT_FRAGMENT_NODE: 357 case ATTRIBUTE_NODE: 358 case TEXT_NODE: 359 case CDATA_SECTION_NODE: 360 case COMMENT_NODE: 361 return null; 362 363 default: 364 throw new DOMException(DOMException.NOT_SUPPORTED_ERR, 365 "Unsupported node type " + getNodeType()); 366 } 367 } 368 369 private String getParentBaseUri() { 370 Node parentNode = getParentNode(); 371 return parentNode != null ? parentNode.getBaseURI() : null; 372 } 373 374 /** 375 * Returns the sanitized input if it is a URI, or {@code null} otherwise. 376 */ 377 private String sanitizeUri(String uri) { 378 if (uri == null || uri.length() == 0) { 379 return null; 380 } 381 try { 382 return new URI(uri).toString(); 383 } catch (URISyntaxException e) { 384 return null; 385 } 386 } 387 388 public short compareDocumentPosition(Node other) 389 throws DOMException { 390 throw new UnsupportedOperationException(); // TODO 391 } 392 393 public String getTextContent() throws DOMException { 394 return getNodeValue(); 395 } 396 397 void getTextContent(StringBuilder buf) throws DOMException { 398 String content = getNodeValue(); 399 if (content != null) { 400 buf.append(content); 401 } 402 } 403 404 public final void setTextContent(String textContent) throws DOMException { 405 switch (getNodeType()) { 406 case DOCUMENT_TYPE_NODE: 407 case DOCUMENT_NODE: 408 return; // do nothing! 409 410 case ELEMENT_NODE: 411 case ENTITY_NODE: 412 case ENTITY_REFERENCE_NODE: 413 case DOCUMENT_FRAGMENT_NODE: 414 // remove all existing children 415 Node child; 416 while ((child = getFirstChild()) != null) { 417 removeChild(child); 418 } 419 // create a text node to hold the given content 420 if (textContent != null && textContent.length() != 0) { 421 appendChild(document.createTextNode(textContent)); 422 } 423 return; 424 425 case ATTRIBUTE_NODE: 426 case TEXT_NODE: 427 case CDATA_SECTION_NODE: 428 case PROCESSING_INSTRUCTION_NODE: 429 case COMMENT_NODE: 430 case NOTATION_NODE: 431 setNodeValue(textContent); 432 return; 433 434 default: 435 throw new DOMException(DOMException.NOT_SUPPORTED_ERR, 436 "Unsupported node type " + getNodeType()); 437 } 438 } 439 440 public boolean isSameNode(Node other) { 441 return this == other; 442 } 443 444 /** 445 * Returns the element whose namespace definitions apply to this node. Use 446 * this element when mapping prefixes to URIs and vice versa. 447 */ 448 private NodeImpl getNamespacingElement() { 449 switch (this.getNodeType()) { 450 case ELEMENT_NODE: 451 return this; 452 453 case DOCUMENT_NODE: 454 return (NodeImpl) ((Document) this).getDocumentElement(); 455 456 case ENTITY_NODE: 457 case NOTATION_NODE: 458 case DOCUMENT_FRAGMENT_NODE: 459 case DOCUMENT_TYPE_NODE: 460 return null; 461 462 case ATTRIBUTE_NODE: 463 return (NodeImpl) ((Attr) this).getOwnerElement(); 464 465 case TEXT_NODE: 466 case CDATA_SECTION_NODE: 467 case ENTITY_REFERENCE_NODE: 468 case PROCESSING_INSTRUCTION_NODE: 469 case COMMENT_NODE: 470 return getContainingElement(); 471 472 default: 473 throw new DOMException(DOMException.NOT_SUPPORTED_ERR, 474 "Unsupported node type " + getNodeType()); 475 } 476 } 477 478 /** 479 * Returns the nearest ancestor element that contains this node. 480 */ 481 private NodeImpl getContainingElement() { 482 for (Node p = getParentNode(); p != null; p = p.getParentNode()) { 483 if (p.getNodeType() == ELEMENT_NODE) { 484 return (NodeImpl) p; 485 } 486 } 487 return null; 488 } 489 490 public final String lookupPrefix(String namespaceURI) { 491 if (namespaceURI == null) { 492 return null; 493 } 494 495 // the XML specs define some prefixes (like "xml" and "xmlns") but this 496 // API is explicitly defined to ignore those. 497 498 NodeImpl target = getNamespacingElement(); 499 for (NodeImpl node = target; node != null; node = node.getContainingElement()) { 500 // check this element's namespace first 501 if (namespaceURI.equals(node.getNamespaceURI()) 502 && target.isPrefixMappedToUri(node.getPrefix(), namespaceURI)) { 503 return node.getPrefix(); 504 } 505 506 // search this element for an attribute of this form: 507 // xmlns:foo="http://namespaceURI" 508 if (!node.hasAttributes()) { 509 continue; 510 } 511 NamedNodeMap attributes = node.getAttributes(); 512 for (int i = 0, length = attributes.getLength(); i < length; i++) { 513 Node attr = attributes.item(i); 514 if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI()) 515 || !"xmlns".equals(attr.getPrefix()) 516 || !namespaceURI.equals(attr.getNodeValue())) { 517 continue; 518 } 519 if (target.isPrefixMappedToUri(attr.getLocalName(), namespaceURI)) { 520 return attr.getLocalName(); 521 } 522 } 523 } 524 525 return null; 526 } 527 528 /** 529 * Returns true if the given prefix is mapped to the given URI on this 530 * element. Since child elements can redefine prefixes, this check is 531 * necessary: {@code 532 * <foo xmlns:a="http://good"> 533 * <bar xmlns:a="http://evil"> 534 * <a:baz /> 535 * </bar> 536 * </foo>} 537 * 538 * @param prefix the prefix to find. Nullable. 539 * @param uri the URI to match. Non-null. 540 */ 541 boolean isPrefixMappedToUri(String prefix, String uri) { 542 if (prefix == null) { 543 return false; 544 } 545 546 String actual = lookupNamespaceURI(prefix); 547 return uri.equals(actual); 548 } 549 550 public final boolean isDefaultNamespace(String namespaceURI) { 551 String actual = lookupNamespaceURI(null); // null yields the default namespace 552 return namespaceURI == null 553 ? actual == null 554 : namespaceURI.equals(actual); 555 } 556 557 public final String lookupNamespaceURI(String prefix) { 558 NodeImpl target = getNamespacingElement(); 559 for (NodeImpl node = target; node != null; node = node.getContainingElement()) { 560 // check this element's namespace first 561 String nodePrefix = node.getPrefix(); 562 if (node.getNamespaceURI() != null) { 563 if (prefix == null // null => default prefix 564 ? nodePrefix == null 565 : prefix.equals(nodePrefix)) { 566 return node.getNamespaceURI(); 567 } 568 } 569 570 // search this element for an attribute of the appropriate form. 571 // default namespace: xmlns="http://resultUri" 572 // non default: xmlns:specifiedPrefix="http://resultUri" 573 if (!node.hasAttributes()) { 574 continue; 575 } 576 NamedNodeMap attributes = node.getAttributes(); 577 for (int i = 0, length = attributes.getLength(); i < length; i++) { 578 Node attr = attributes.item(i); 579 if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())) { 580 continue; 581 } 582 if (prefix == null // null => default prefix 583 ? "xmlns".equals(attr.getNodeName()) 584 : "xmlns".equals(attr.getPrefix()) && prefix.equals(attr.getLocalName())) { 585 String value = attr.getNodeValue(); 586 return value.length() > 0 ? value : null; 587 } 588 } 589 } 590 591 return null; 592 } 593 594 /** 595 * Returns a list of objects such that two nodes are equal if their lists 596 * are equal. Be careful: the lists may contain NamedNodeMaps and Nodes, 597 * neither of which override Object.equals(). Such values must be compared 598 * manually. 599 */ 600 private static List<Object> createEqualityKey(Node node) { 601 List<Object> values = new ArrayList<Object>(); 602 values.add(node.getNodeType()); 603 values.add(node.getNodeName()); 604 values.add(node.getLocalName()); 605 values.add(node.getNamespaceURI()); 606 values.add(node.getPrefix()); 607 values.add(node.getNodeValue()); 608 for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { 609 values.add(child); 610 } 611 612 switch (node.getNodeType()) { 613 case DOCUMENT_TYPE_NODE: 614 DocumentTypeImpl doctype = (DocumentTypeImpl) node; 615 values.add(doctype.getPublicId()); 616 values.add(doctype.getSystemId()); 617 values.add(doctype.getInternalSubset()); 618 values.add(doctype.getEntities()); 619 values.add(doctype.getNotations()); 620 break; 621 622 case ELEMENT_NODE: 623 Element element = (Element) node; 624 values.add(element.getAttributes()); 625 break; 626 } 627 628 return values; 629 } 630 631 public final boolean isEqualNode(Node arg) { 632 if (arg == this) { 633 return true; 634 } 635 636 List<Object> listA = createEqualityKey(this); 637 List<Object> listB = createEqualityKey(arg); 638 639 if (listA.size() != listB.size()) { 640 return false; 641 } 642 643 for (int i = 0; i < listA.size(); i++) { 644 Object a = listA.get(i); 645 Object b = listB.get(i); 646 647 if (a == b) { 648 continue; 649 650 } else if (a == null || b == null) { 651 return false; 652 653 } else if (a instanceof String || a instanceof Short) { 654 if (!a.equals(b)) { 655 return false; 656 } 657 658 } else if (a instanceof NamedNodeMap) { 659 if (!(b instanceof NamedNodeMap) 660 || !namedNodeMapsEqual((NamedNodeMap) a, (NamedNodeMap) b)) { 661 return false; 662 } 663 664 } else if (a instanceof Node) { 665 if (!(b instanceof Node) 666 || !((Node) a).isEqualNode((Node) b)) { 667 return false; 668 } 669 670 } else { 671 throw new AssertionError(); // unexpected type 672 } 673 } 674 675 return true; 676 } 677 678 private boolean namedNodeMapsEqual(NamedNodeMap a, NamedNodeMap b) { 679 if (a.getLength() != b.getLength()) { 680 return false; 681 } 682 for (int i = 0; i < a.getLength(); i++) { 683 Node aNode = a.item(i); 684 Node bNode = aNode.getLocalName() == null 685 ? b.getNamedItem(aNode.getNodeName()) 686 : b.getNamedItemNS(aNode.getNamespaceURI(), aNode.getLocalName()); 687 if (bNode == null || !aNode.isEqualNode(bNode)) { 688 return false; 689 } 690 } 691 return true; 692 } 693 694 public final Object getFeature(String feature, String version) { 695 return isSupported(feature, version) ? this : null; 696 } 697 698 public final Object setUserData(String key, Object data, UserDataHandler handler) { 699 if (key == null) { 700 throw new NullPointerException("key == null"); 701 } 702 Map<String, UserData> map = document.getUserDataMap(this); 703 UserData previous = data == null 704 ? map.remove(key) 705 : map.put(key, new UserData(data, handler)); 706 return previous != null ? previous.value : null; 707 } 708 709 public final Object getUserData(String key) { 710 if (key == null) { 711 throw new NullPointerException("key == null"); 712 } 713 Map<String, UserData> map = document.getUserDataMapForRead(this); 714 UserData userData = map.get(key); 715 return userData != null ? userData.value : null; 716 } 717 718 static class UserData { 719 final Object value; 720 final UserDataHandler handler; 721 UserData(Object value, UserDataHandler handler) { 722 this.value = value; 723 this.handler = handler; 724 } 725 } 726 } 727