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