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