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