NodeImpl.java revision f33eae7e84eb6d3b0f4e86b59605bb3de73009f3
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 org.apache.xml.serializer.utils.SystemIDResolver; 20import org.apache.xml.utils.URI; 21import org.w3c.dom.Attr; 22import org.w3c.dom.CharacterData; 23import org.w3c.dom.DOMException; 24import org.w3c.dom.Document; 25import org.w3c.dom.Element; 26import org.w3c.dom.NamedNodeMap; 27import org.w3c.dom.Node; 28import org.w3c.dom.NodeList; 29import org.w3c.dom.ProcessingInstruction; 30import org.w3c.dom.TypeInfo; 31import org.w3c.dom.UserDataHandler; 32 33import javax.xml.transform.TransformerException; 34import java.util.ArrayList; 35import java.util.List; 36import java.util.Map; 37 38/** 39 * A straightforward implementation of the corresponding W3C DOM node. 40 * 41 * <p>Some fields have package visibility so other classes can access them while 42 * maintaining the DOM structure. 43 * 44 * <p>This class represents a Node that has neither a parent nor children. 45 * Subclasses may have either. 46 * 47 * <p>Some code was adapted from Apache Xerces. 48 */ 49public abstract class NodeImpl implements Node { 50 51 private static final NodeList EMPTY_LIST = new NodeListImpl(); 52 53 static final TypeInfo NULL_TYPE_INFO = new TypeInfo() { 54 public String getTypeName() { 55 return null; 56 } 57 public String getTypeNamespace() { 58 return null; 59 } 60 public boolean isDerivedFrom( 61 String typeNamespaceArg, String typeNameArg, int derivationMethod) { 62 return false; 63 } 64 }; 65 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 the element or attribute node to be namespace-aware and assign it 220 * the specified name and namespace URI. 221 * 222 * @param node an AttrImpl or ElementImpl 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 * Checks whether a required string matches an actual string. This utility 272 * method is used for comparing namespaces and such. It takes into account 273 * null arguments and the "*" special case. 274 * 275 * @param required The required string. 276 * @param actual The actual string. 277 * @return True if and only if the actual string matches the required one. 278 */ 279 private static boolean matchesName(String required, String actual, boolean wildcard) { 280 if (wildcard && "*".equals(required)) { 281 return true; 282 } 283 284 if (required == null) { 285 return (actual == null); 286 } 287 288 return required.equals(actual); 289 } 290 291 /** 292 * Checks whether this node's name matches a required name. It takes into 293 * account null arguments and the "*" special case. 294 * 295 * @param name The required name. 296 * @return True if and only if the actual name matches the required one. 297 */ 298 public boolean matchesName(String name, boolean wildcard) { 299 return matchesName(name, getNodeName(), wildcard); 300 } 301 302 /** 303 * Checks whether this node's namespace and local name match a required 304 * pair of namespace and local name. It takes into account null arguments 305 * and the "*" special case. 306 * 307 * @param namespaceURI The required namespace. 308 * @param localName The required local name. 309 * @return True if and only if the actual namespace and local name match 310 * the required pair of namespace and local name. 311 */ 312 public boolean matchesNameNS(String namespaceURI, String localName, boolean wildcard) { 313 return matchesName(namespaceURI, getNamespaceURI(), wildcard) && matchesName(localName, getLocalName(), wildcard); 314 } 315 316 public final String getBaseURI() { 317 switch (getNodeType()) { 318 case DOCUMENT_NODE: 319 return sanitizeUri(((Document) this).getDocumentURI()); 320 321 case ELEMENT_NODE: 322 Element element = (Element) this; 323 String uri = element.getAttributeNS( 324 "http://www.w3.org/XML/1998/namespace", "base"); // or "xml:base" 325 326 // if this node has no base URI, return the parent's. 327 if (uri == null || uri.length() == 0) { 328 return getParentBaseUri(); 329 } 330 331 // if this node's URI is absolute, return that 332 if (SystemIDResolver.isAbsoluteURI(uri)) { 333 return uri; 334 } 335 336 // this node has a relative URI. Try to resolve it against the 337 // parent, but if that doesn't work just give up and return null. 338 String parentUri = getParentBaseUri(); 339 if (parentUri == null) { 340 return null; 341 } 342 try { 343 return SystemIDResolver.getAbsoluteURI(uri, parentUri); 344 } catch (TransformerException e) { 345 return null; // the spec requires that we swallow exceptions 346 } 347 348 case PROCESSING_INSTRUCTION_NODE: 349 return getParentBaseUri(); 350 351 case NOTATION_NODE: 352 case ENTITY_NODE: 353 // When we support these node types, the parser should 354 // initialize a base URI field on these nodes. 355 return null; 356 357 case ENTITY_REFERENCE_NODE: 358 // TODO: get this value from the parser, falling back to the 359 // referenced entity's baseURI if that doesn't exist 360 return null; 361 362 case DOCUMENT_TYPE_NODE: 363 case DOCUMENT_FRAGMENT_NODE: 364 case ATTRIBUTE_NODE: 365 case TEXT_NODE: 366 case CDATA_SECTION_NODE: 367 case COMMENT_NODE: 368 return null; 369 370 default: 371 throw new DOMException(DOMException.NOT_SUPPORTED_ERR, 372 "Unsupported node type " + getNodeType()); 373 } 374 } 375 376 private String getParentBaseUri() { 377 Node parentNode = getParentNode(); 378 return parentNode != null ? parentNode.getBaseURI() : null; 379 } 380 381 /** 382 * Returns the sanitized input if it is a URI, or {@code null} otherwise. 383 */ 384 private String sanitizeUri(String uri) { 385 if (uri == null || uri.length() == 0) { 386 return null; 387 } 388 try { 389 return new URI(uri).toString(); 390 } catch (URI.MalformedURIException e) { 391 return null; 392 } 393 } 394 395 public short compareDocumentPosition(Node other) 396 throws DOMException { 397 throw new UnsupportedOperationException(); // TODO 398 } 399 400 public String getTextContent() throws DOMException { 401 return getNodeValue(); 402 } 403 404 void getTextContent(StringBuilder buf) throws DOMException { 405 String content = getNodeValue(); 406 if (content != null) { 407 buf.append(content); 408 } 409 } 410 411 public final void setTextContent(String textContent) throws DOMException { 412 switch (getNodeType()) { 413 case DOCUMENT_TYPE_NODE: 414 case DOCUMENT_NODE: 415 return; // do nothing! 416 417 case ELEMENT_NODE: 418 case ENTITY_NODE: 419 case ENTITY_REFERENCE_NODE: 420 case DOCUMENT_FRAGMENT_NODE: 421 // remove all existing children 422 Node child; 423 while ((child = getFirstChild()) != null) { 424 removeChild(child); 425 } 426 // create a text node to hold the given content 427 if (textContent != null && textContent.length() != 0) { 428 appendChild(document.createTextNode(textContent)); 429 } 430 return; 431 432 case ATTRIBUTE_NODE: 433 case TEXT_NODE: 434 case CDATA_SECTION_NODE: 435 case PROCESSING_INSTRUCTION_NODE: 436 case COMMENT_NODE: 437 case NOTATION_NODE: 438 setNodeValue(textContent); 439 return; 440 441 default: 442 throw new DOMException(DOMException.NOT_SUPPORTED_ERR, 443 "Unsupported node type " + getNodeType()); 444 } 445 } 446 447 public boolean isSameNode(Node other) { 448 return this == other; 449 } 450 451 /** 452 * Returns the element whose namespace definitions apply to this node. Use 453 * this element when mapping prefixes to URIs and vice versa. 454 */ 455 private NodeImpl getNamespacingElement() { 456 switch (this.getNodeType()) { 457 case ELEMENT_NODE: 458 return this; 459 460 case DOCUMENT_NODE: 461 return (NodeImpl) ((Document) this).getDocumentElement(); 462 463 case ENTITY_NODE: 464 case NOTATION_NODE: 465 case DOCUMENT_FRAGMENT_NODE: 466 case DOCUMENT_TYPE_NODE: 467 return null; 468 469 case ATTRIBUTE_NODE: 470 return (NodeImpl) ((Attr) this).getOwnerElement(); 471 472 case TEXT_NODE: 473 case CDATA_SECTION_NODE: 474 case ENTITY_REFERENCE_NODE: 475 case PROCESSING_INSTRUCTION_NODE: 476 case COMMENT_NODE: 477 return getContainingElement(); 478 479 default: 480 throw new DOMException(DOMException.NOT_SUPPORTED_ERR, 481 "Unsupported node type " + getNodeType()); 482 } 483 } 484 485 /** 486 * Returns the nearest ancestor element that contains this node. 487 */ 488 private NodeImpl getContainingElement() { 489 for (Node p = getParentNode(); p != null; p = p.getParentNode()) { 490 if (p.getNodeType() == ELEMENT_NODE) { 491 return (NodeImpl) p; 492 } 493 } 494 return null; 495 } 496 497 public final String lookupPrefix(String namespaceURI) { 498 if (namespaceURI == null) { 499 return null; 500 } 501 502 // the XML specs define some prefixes (like "xml" and "xmlns") but this 503 // API is explicitly defined to ignore those. 504 505 NodeImpl target = getNamespacingElement(); 506 for (NodeImpl node = target; node != null; node = node.getContainingElement()) { 507 // check this element's namespace first 508 if (namespaceURI.equals(node.getNamespaceURI()) 509 && target.isPrefixMappedToUri(node.getPrefix(), namespaceURI)) { 510 return node.getPrefix(); 511 } 512 513 // search this element for an attribute of this form: 514 // xmlns:foo="http://namespaceURI" 515 if (!node.hasAttributes()) { 516 continue; 517 } 518 NamedNodeMap attributes = node.getAttributes(); 519 for (int i = 0, length = attributes.getLength(); i < length; i++) { 520 Node attr = attributes.item(i); 521 if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI()) 522 || !"xmlns".equals(attr.getPrefix()) 523 || !namespaceURI.equals(attr.getNodeValue())) { 524 continue; 525 } 526 if (target.isPrefixMappedToUri(attr.getLocalName(), namespaceURI)) { 527 return attr.getLocalName(); 528 } 529 } 530 } 531 532 return null; 533 } 534 535 /** 536 * Returns true if the given prefix is mapped to the given URI on this 537 * element. Since child elements can redefine prefixes, this check is 538 * necessary: {@code 539 * <foo xmlns:a="http://good"> 540 * <bar xmlns:a="http://evil"> 541 * <a:baz /> 542 * </bar> 543 * </foo>} 544 * 545 * @param prefix the prefix to find. Nullable. 546 * @param uri the URI to match. Non-null. 547 */ 548 boolean isPrefixMappedToUri(String prefix, String uri) { 549 if (prefix == null) { 550 return false; 551 } 552 553 String actual = lookupNamespaceURI(prefix); 554 return uri.equals(actual); 555 } 556 557 public final boolean isDefaultNamespace(String namespaceURI) { 558 String actual = lookupNamespaceURI(null); // null yields the default namespace 559 return namespaceURI == null 560 ? actual == null 561 : namespaceURI.equals(actual); 562 } 563 564 public final String lookupNamespaceURI(String prefix) { 565 NodeImpl target = getNamespacingElement(); 566 for (NodeImpl node = target; node != null; node = node.getContainingElement()) { 567 // check this element's namespace first 568 String nodePrefix = node.getPrefix(); 569 if (node.getNamespaceURI() != null) { 570 if (prefix == null // null => default prefix 571 ? nodePrefix == null 572 : prefix.equals(nodePrefix)) { 573 return node.getNamespaceURI(); 574 } 575 } 576 577 // search this element for an attribute of the appropriate form. 578 // default namespace: xmlns="http://resultUri" 579 // non default: xmlns:specifiedPrefix="http://resultUri" 580 if (!node.hasAttributes()) { 581 continue; 582 } 583 NamedNodeMap attributes = node.getAttributes(); 584 for (int i = 0, length = attributes.getLength(); i < length; i++) { 585 Node attr = attributes.item(i); 586 if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())) { 587 continue; 588 } 589 if (prefix == null // null => default prefix 590 ? "xmlns".equals(attr.getNodeName()) 591 : "xmlns".equals(attr.getPrefix()) && prefix.equals(attr.getLocalName())) { 592 String value = attr.getNodeValue(); 593 return value.length() > 0 ? value : null; 594 } 595 } 596 } 597 598 return null; 599 } 600 601 /** 602 * Returns a list of objects such that two nodes are equal if their lists 603 * are equal. Be careful: the lists may contain NamedNodeMaps and Nodes, 604 * neither of which override Object.equals(). Such values must be compared 605 * manually. 606 */ 607 private static List<Object> createEqualityKey(Node node) { 608 List<Object> values = new ArrayList<Object>(); 609 values.add(node.getNodeType()); 610 values.add(node.getNodeName()); 611 values.add(node.getLocalName()); 612 values.add(node.getNamespaceURI()); 613 values.add(node.getPrefix()); 614 values.add(node.getNodeValue()); 615 for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { 616 values.add(child); 617 } 618 619 switch (node.getNodeType()) { 620 case DOCUMENT_TYPE_NODE: 621 DocumentTypeImpl doctype = (DocumentTypeImpl) node; 622 values.add(doctype.getPublicId()); 623 values.add(doctype.getSystemId()); 624 values.add(doctype.getInternalSubset()); 625 values.add(doctype.getEntities()); 626 values.add(doctype.getNotations()); 627 break; 628 629 case ELEMENT_NODE: 630 Element element = (Element) node; 631 values.add(element.getAttributes()); 632 break; 633 } 634 635 return values; 636 } 637 638 public final boolean isEqualNode(Node arg) { 639 if (arg == this) { 640 return true; 641 } 642 643 List<Object> listA = createEqualityKey(this); 644 List<Object> listB = createEqualityKey(arg); 645 646 if (listA.size() != listB.size()) { 647 return false; 648 } 649 650 for (int i = 0; i < listA.size(); i++) { 651 Object a = listA.get(i); 652 Object b = listB.get(i); 653 654 if (a == b) { 655 continue; 656 657 } else if (a == null || b == null) { 658 return false; 659 660 } else if (a instanceof String || a instanceof Short) { 661 if (!a.equals(b)) { 662 return false; 663 } 664 665 } else if (a instanceof NamedNodeMap) { 666 if (!(b instanceof NamedNodeMap) 667 || !namedNodeMapsEqual((NamedNodeMap) a, (NamedNodeMap) b)) { 668 return false; 669 } 670 671 } else if (a instanceof Node) { 672 if (!(b instanceof Node) 673 || !((Node) a).isEqualNode((Node) b)) { 674 return false; 675 } 676 677 } else { 678 throw new AssertionError(); // unexpected type 679 } 680 } 681 682 return true; 683 } 684 685 private boolean namedNodeMapsEqual(NamedNodeMap a, NamedNodeMap b) { 686 if (a.getLength() != b.getLength()) { 687 return false; 688 } 689 for (int i = 0; i < a.getLength(); i++) { 690 Node aNode = a.item(i); 691 Node bNode = aNode.getLocalName() == null 692 ? b.getNamedItem(aNode.getNodeName()) 693 : b.getNamedItemNS(aNode.getNamespaceURI(), aNode.getLocalName()); 694 if (bNode == null || !aNode.isEqualNode(bNode)) { 695 return false; 696 } 697 } 698 return true; 699 } 700 701 public final Object getFeature(String feature, String version) { 702 return isSupported(feature, version) ? this : null; 703 } 704 705 public final Object setUserData(String key, Object data, UserDataHandler handler) { 706 if (key == null) { 707 throw new NullPointerException(); 708 } 709 Map<String, UserData> map = document.getUserDataMap(this); 710 UserData previous = data == null 711 ? map.remove(key) 712 : map.put(key, new UserData(data, handler)); 713 return previous != null ? previous.value : null; 714 } 715 716 public final Object getUserData(String key) { 717 if (key == null) { 718 throw new NullPointerException(); 719 } 720 Map<String, UserData> map = document.getUserDataMapForRead(this); 721 UserData userData = map.get(key); 722 return userData != null ? userData.value : null; 723 } 724 725 static class UserData { 726 final Object value; 727 final UserDataHandler handler; 728 UserData(Object value, UserDataHandler handler) { 729 this.value = value; 730 this.handler = handler; 731 } 732 } 733} 734