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