001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.commons.jxpath.ri.model.dom; 018 019 import java.util.HashMap; 020 import java.util.Locale; 021 import java.util.Map; 022 023 import org.apache.commons.jxpath.JXPathAbstractFactoryException; 024 import org.apache.commons.jxpath.JXPathContext; 025 import org.apache.commons.jxpath.JXPathException; 026 import org.apache.commons.jxpath.Pointer; 027 import org.apache.commons.jxpath.ri.Compiler; 028 import org.apache.commons.jxpath.ri.NamespaceResolver; 029 import org.apache.commons.jxpath.ri.QName; 030 import org.apache.commons.jxpath.ri.compiler.NodeNameTest; 031 import org.apache.commons.jxpath.ri.compiler.NodeTest; 032 import org.apache.commons.jxpath.ri.compiler.NodeTypeTest; 033 import org.apache.commons.jxpath.ri.compiler.ProcessingInstructionTest; 034 import org.apache.commons.jxpath.ri.model.NodeIterator; 035 import org.apache.commons.jxpath.ri.model.NodePointer; 036 import org.apache.commons.jxpath.ri.model.beans.NullPointer; 037 import org.apache.commons.jxpath.util.TypeUtils; 038 import org.w3c.dom.Attr; 039 import org.w3c.dom.Comment; 040 import org.w3c.dom.Document; 041 import org.w3c.dom.Element; 042 import org.w3c.dom.NamedNodeMap; 043 import org.w3c.dom.Node; 044 import org.w3c.dom.NodeList; 045 import org.w3c.dom.ProcessingInstruction; 046 047 /** 048 * A Pointer that points to a DOM node. Because a DOM Node is not guaranteed Serializable, 049 * a DOMNodePointer instance may likewise not be properly Serializable. 050 * 051 * @author Dmitri Plotnikov 052 * @version $Revision: 668329 $ $Date: 2008-06-16 16:59:48 -0500 (Mon, 16 Jun 2008) $ 053 */ 054 public class DOMNodePointer extends NodePointer { 055 056 private static final long serialVersionUID = -8751046933894857319L; 057 058 private Node node; 059 private Map namespaces; 060 private String defaultNamespace; 061 private String id; 062 private NamespaceResolver localNamespaceResolver; 063 064 /** XML namespace URI */ 065 public static final String XML_NAMESPACE_URI = 066 "http://www.w3.org/XML/1998/namespace"; 067 068 /** XMLNS namespace URI */ 069 public static final String XMLNS_NAMESPACE_URI = 070 "http://www.w3.org/2000/xmlns/"; 071 072 /** 073 * Create a new DOMNodePointer. 074 * @param node pointed at 075 * @param locale Locale 076 */ 077 public DOMNodePointer(Node node, Locale locale) { 078 super(null, locale); 079 this.node = node; 080 } 081 082 /** 083 * Create a new DOMNodePointer. 084 * @param node pointed at 085 * @param locale Locale 086 * @param id string id 087 */ 088 public DOMNodePointer(Node node, Locale locale, String id) { 089 super(null, locale); 090 this.node = node; 091 this.id = id; 092 } 093 094 /** 095 * Create a new DOMNodePointer. 096 * @param parent pointer 097 * @param node pointed 098 */ 099 public DOMNodePointer(NodePointer parent, Node node) { 100 super(parent); 101 this.node = node; 102 } 103 104 public boolean testNode(NodeTest test) { 105 return testNode(node, test); 106 } 107 108 /** 109 * Test a Node. 110 * @param node to test 111 * @param test to execute 112 * @return true if node passes test 113 */ 114 public static boolean testNode(Node node, NodeTest test) { 115 if (test == null) { 116 return true; 117 } 118 if (test instanceof NodeNameTest) { 119 if (node.getNodeType() != Node.ELEMENT_NODE) { 120 return false; 121 } 122 123 NodeNameTest nodeNameTest = (NodeNameTest) test; 124 QName testName = nodeNameTest.getNodeName(); 125 String namespaceURI = nodeNameTest.getNamespaceURI(); 126 boolean wildcard = nodeNameTest.isWildcard(); 127 String testPrefix = testName.getPrefix(); 128 if (wildcard && testPrefix == null) { 129 return true; 130 } 131 if (wildcard 132 || testName.getName() 133 .equals(DOMNodePointer.getLocalName(node))) { 134 String nodeNS = DOMNodePointer.getNamespaceURI(node); 135 return equalStrings(namespaceURI, nodeNS) || nodeNS == null 136 && equalStrings(testPrefix, getPrefix(node)); 137 } 138 return false; 139 } 140 if (test instanceof NodeTypeTest) { 141 int nodeType = node.getNodeType(); 142 switch (((NodeTypeTest) test).getNodeType()) { 143 case Compiler.NODE_TYPE_NODE : 144 return true; 145 case Compiler.NODE_TYPE_TEXT : 146 return nodeType == Node.CDATA_SECTION_NODE 147 || nodeType == Node.TEXT_NODE; 148 case Compiler.NODE_TYPE_COMMENT : 149 return nodeType == Node.COMMENT_NODE; 150 case Compiler.NODE_TYPE_PI : 151 return nodeType == Node.PROCESSING_INSTRUCTION_NODE; 152 default: 153 return false; 154 } 155 } 156 if (test instanceof ProcessingInstructionTest 157 && node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) { 158 String testPI = ((ProcessingInstructionTest) test).getTarget(); 159 String nodePI = ((ProcessingInstruction) node).getTarget(); 160 return testPI.equals(nodePI); 161 } 162 return false; 163 } 164 165 /** 166 * Test string equality. 167 * @param s1 String 1 168 * @param s2 String 2 169 * @return true if == or .equals() 170 */ 171 private static boolean equalStrings(String s1, String s2) { 172 if (s1 == s2) { 173 return true; 174 } 175 s1 = s1 == null ? "" : s1.trim(); 176 s2 = s2 == null ? "" : s2.trim(); 177 return s1.equals(s2); 178 } 179 180 public QName getName() { 181 String ln = null; 182 String ns = null; 183 int type = node.getNodeType(); 184 if (type == Node.ELEMENT_NODE) { 185 ns = DOMNodePointer.getPrefix(node); 186 ln = DOMNodePointer.getLocalName(node); 187 } 188 else if (type == Node.PROCESSING_INSTRUCTION_NODE) { 189 ln = ((ProcessingInstruction) node).getTarget(); 190 } 191 return new QName(ns, ln); 192 } 193 194 public String getNamespaceURI() { 195 return getNamespaceURI(node); 196 } 197 198 public NodeIterator childIterator(NodeTest test, boolean reverse, 199 NodePointer startWith) { 200 return new DOMNodeIterator(this, test, reverse, startWith); 201 } 202 203 public NodeIterator attributeIterator(QName name) { 204 return new DOMAttributeIterator(this, name); 205 } 206 207 public NodePointer namespacePointer(String prefix) { 208 return new NamespacePointer(this, prefix); 209 } 210 211 public NodeIterator namespaceIterator() { 212 return new DOMNamespaceIterator(this); 213 } 214 215 public synchronized NamespaceResolver getNamespaceResolver() { 216 if (localNamespaceResolver == null) { 217 localNamespaceResolver = new NamespaceResolver(super.getNamespaceResolver()); 218 localNamespaceResolver.setNamespaceContextPointer(this); 219 } 220 return localNamespaceResolver; 221 } 222 223 public String getNamespaceURI(String prefix) { 224 if (prefix == null || prefix.equals("")) { 225 return getDefaultNamespaceURI(); 226 } 227 228 if (prefix.equals("xml")) { 229 return XML_NAMESPACE_URI; 230 } 231 232 if (prefix.equals("xmlns")) { 233 return XMLNS_NAMESPACE_URI; 234 } 235 236 String namespace = null; 237 if (namespaces == null) { 238 namespaces = new HashMap(); 239 } 240 else { 241 namespace = (String) namespaces.get(prefix); 242 } 243 244 if (namespace == null) { 245 String qname = "xmlns:" + prefix; 246 Node aNode = node; 247 if (aNode instanceof Document) { 248 aNode = ((Document) aNode).getDocumentElement(); 249 } 250 while (aNode != null) { 251 if (aNode.getNodeType() == Node.ELEMENT_NODE) { 252 Attr attr = ((Element) aNode).getAttributeNode(qname); 253 if (attr != null) { 254 namespace = attr.getValue(); 255 break; 256 } 257 } 258 aNode = aNode.getParentNode(); 259 } 260 if (namespace == null || namespace.equals("")) { 261 namespace = NodePointer.UNKNOWN_NAMESPACE; 262 } 263 } 264 265 namespaces.put(prefix, namespace); 266 if (namespace == UNKNOWN_NAMESPACE) { 267 return null; 268 } 269 270 // TBD: We are supposed to resolve relative URIs to absolute ones. 271 return namespace; 272 } 273 274 public String getDefaultNamespaceURI() { 275 if (defaultNamespace == null) { 276 Node aNode = node; 277 if (aNode instanceof Document) { 278 aNode = ((Document) aNode).getDocumentElement(); 279 } 280 while (aNode != null) { 281 if (aNode.getNodeType() == Node.ELEMENT_NODE) { 282 Attr attr = ((Element) aNode).getAttributeNode("xmlns"); 283 if (attr != null) { 284 defaultNamespace = attr.getValue(); 285 break; 286 } 287 } 288 aNode = aNode.getParentNode(); 289 } 290 } 291 if (defaultNamespace == null) { 292 defaultNamespace = ""; 293 } 294 // TBD: We are supposed to resolve relative URIs to absolute ones. 295 return defaultNamespace.equals("") ? null : defaultNamespace; 296 } 297 298 public Object getBaseValue() { 299 return node; 300 } 301 302 public Object getImmediateNode() { 303 return node; 304 } 305 306 public boolean isActual() { 307 return true; 308 } 309 310 public boolean isCollection() { 311 return false; 312 } 313 314 public int getLength() { 315 return 1; 316 } 317 318 public boolean isLeaf() { 319 return !node.hasChildNodes(); 320 } 321 322 /** 323 * Returns true if the xml:lang attribute for the current node 324 * or its parent has the specified prefix <i>lang</i>. 325 * If no node has this prefix, calls <code>super.isLanguage(lang)</code>. 326 * @param lang ns to test 327 * @return boolean 328 */ 329 public boolean isLanguage(String lang) { 330 String current = getLanguage(); 331 return current == null ? super.isLanguage(lang) 332 : current.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH)); 333 } 334 335 /** 336 * Find the nearest occurrence of the specified attribute 337 * on the specified and enclosing elements. 338 * @param n current node 339 * @param attrName attribute name 340 * @return attribute value 341 */ 342 protected static String findEnclosingAttribute(Node n, String attrName) { 343 while (n != null) { 344 if (n.getNodeType() == Node.ELEMENT_NODE) { 345 Element e = (Element) n; 346 String attr = e.getAttribute(attrName); 347 if (attr != null && !attr.equals("")) { 348 return attr; 349 } 350 } 351 n = n.getParentNode(); 352 } 353 return null; 354 } 355 356 /** 357 * Get the language attribute for this node. 358 * @return String language name 359 */ 360 protected String getLanguage() { 361 return findEnclosingAttribute(node, "xml:lang"); 362 } 363 364 /** 365 * Sets contents of the node to the specified value. If the value is 366 * a String, the contents of the node are replaced with this text. 367 * If the value is an Element or Document, the children of the 368 * node are replaced with the children of the passed node. 369 * @param value to set 370 */ 371 public void setValue(Object value) { 372 if (node.getNodeType() == Node.TEXT_NODE 373 || node.getNodeType() == Node.CDATA_SECTION_NODE) { 374 String string = (String) TypeUtils.convert(value, String.class); 375 if (string != null && !string.equals("")) { 376 node.setNodeValue(string); 377 } 378 else { 379 node.getParentNode().removeChild(node); 380 } 381 } 382 else { 383 NodeList children = node.getChildNodes(); 384 int count = children.getLength(); 385 for (int i = count; --i >= 0;) { 386 Node child = children.item(i); 387 node.removeChild(child); 388 } 389 390 if (value instanceof Node) { 391 Node valueNode = (Node) value; 392 if (valueNode instanceof Element 393 || valueNode instanceof Document) { 394 children = valueNode.getChildNodes(); 395 for (int i = 0; i < children.getLength(); i++) { 396 Node child = children.item(i); 397 node.appendChild(child.cloneNode(true)); 398 } 399 } 400 else { 401 node.appendChild(valueNode.cloneNode(true)); 402 } 403 } 404 else { 405 String string = (String) TypeUtils.convert(value, String.class); 406 if (string != null && !string.equals("")) { 407 Node textNode = 408 node.getOwnerDocument().createTextNode(string); 409 node.appendChild(textNode); 410 } 411 } 412 } 413 } 414 415 public NodePointer createChild(JXPathContext context, QName name, int index) { 416 if (index == WHOLE_COLLECTION) { 417 index = 0; 418 } 419 boolean success = 420 getAbstractFactory(context).createObject( 421 context, 422 this, 423 node, 424 name.toString(), 425 index); 426 if (success) { 427 NodeTest nodeTest; 428 String prefix = name.getPrefix(); 429 String namespaceURI = prefix == null ? null : context 430 .getNamespaceURI(prefix); 431 nodeTest = new NodeNameTest(name, namespaceURI); 432 433 NodeIterator it = childIterator(nodeTest, false, null); 434 if (it != null && it.setPosition(index + 1)) { 435 return it.getNodePointer(); 436 } 437 } 438 throw new JXPathAbstractFactoryException( 439 "Factory could not create a child node for path: " + asPath() 440 + "/" + name + "[" + (index + 1) + "]"); 441 } 442 443 public NodePointer createChild(JXPathContext context, QName name, 444 int index, Object value) { 445 NodePointer ptr = createChild(context, name, index); 446 ptr.setValue(value); 447 return ptr; 448 } 449 450 public NodePointer createAttribute(JXPathContext context, QName name) { 451 if (!(node instanceof Element)) { 452 return super.createAttribute(context, name); 453 } 454 Element element = (Element) node; 455 String prefix = name.getPrefix(); 456 if (prefix != null) { 457 String ns = null; 458 NamespaceResolver nsr = getNamespaceResolver(); 459 if (nsr != null) { 460 ns = nsr.getNamespaceURI(prefix); 461 } 462 if (ns == null) { 463 throw new JXPathException( 464 "Unknown namespace prefix: " + prefix); 465 } 466 element.setAttributeNS(ns, name.toString(), ""); 467 } 468 else { 469 if (!element.hasAttribute(name.getName())) { 470 element.setAttribute(name.getName(), ""); 471 } 472 } 473 NodeIterator it = attributeIterator(name); 474 it.setPosition(1); 475 return it.getNodePointer(); 476 } 477 478 public void remove() { 479 Node parent = node.getParentNode(); 480 if (parent == null) { 481 throw new JXPathException("Cannot remove root DOM node"); 482 } 483 parent.removeChild(node); 484 } 485 486 public String asPath() { 487 if (id != null) { 488 return "id('" + escape(id) + "')"; 489 } 490 491 StringBuffer buffer = new StringBuffer(); 492 if (parent != null) { 493 buffer.append(parent.asPath()); 494 } 495 switch (node.getNodeType()) { 496 case Node.ELEMENT_NODE : 497 // If the parent pointer is not a DOMNodePointer, it is 498 // the parent's responsibility to produce the node test part 499 // of the path 500 if (parent instanceof DOMNodePointer) { 501 if (buffer.length() == 0 502 || buffer.charAt(buffer.length() - 1) != '/') { 503 buffer.append('/'); 504 } 505 String ln = DOMNodePointer.getLocalName(node); 506 String nsURI = getNamespaceURI(); 507 if (nsURI == null) { 508 buffer.append(ln); 509 buffer.append('['); 510 buffer.append(getRelativePositionByName()).append(']'); 511 } 512 else { 513 String prefix = getNamespaceResolver().getPrefix(nsURI); 514 if (prefix != null) { 515 buffer.append(prefix); 516 buffer.append(':'); 517 buffer.append(ln); 518 buffer.append('['); 519 buffer.append(getRelativePositionByName()); 520 buffer.append(']'); 521 } 522 else { 523 buffer.append("node()"); 524 buffer.append('['); 525 buffer.append(getRelativePositionOfElement()); 526 buffer.append(']'); 527 } 528 } 529 } 530 break; 531 case Node.TEXT_NODE : 532 case Node.CDATA_SECTION_NODE : 533 buffer.append("/text()"); 534 buffer.append('['); 535 buffer.append(getRelativePositionOfTextNode()).append(']'); 536 break; 537 case Node.PROCESSING_INSTRUCTION_NODE : 538 buffer.append("/processing-instruction(\'"); 539 buffer.append(((ProcessingInstruction) node).getTarget()).append("')"); 540 buffer.append('['); 541 buffer.append(getRelativePositionOfPI()).append(']'); 542 break; 543 case Node.DOCUMENT_NODE : 544 // That'll be empty 545 break; 546 default: 547 break; 548 } 549 return buffer.toString(); 550 } 551 552 /** 553 * Get relative position of this among like-named siblings. 554 * @return 1..n 555 */ 556 private int getRelativePositionByName() { 557 int count = 1; 558 Node n = node.getPreviousSibling(); 559 while (n != null) { 560 if (n.getNodeType() == Node.ELEMENT_NODE) { 561 String nm = n.getNodeName(); 562 if (nm.equals(node.getNodeName())) { 563 count++; 564 } 565 } 566 n = n.getPreviousSibling(); 567 } 568 return count; 569 } 570 571 /** 572 * Get relative position of this among all siblings. 573 * @return 1..n 574 */ 575 private int getRelativePositionOfElement() { 576 int count = 1; 577 Node n = node.getPreviousSibling(); 578 while (n != null) { 579 if (n.getNodeType() == Node.ELEMENT_NODE) { 580 count++; 581 } 582 n = n.getPreviousSibling(); 583 } 584 return count; 585 } 586 587 /** 588 * Get the relative position of this among sibling text nodes. 589 * @return 1..n 590 */ 591 private int getRelativePositionOfTextNode() { 592 int count = 1; 593 Node n = node.getPreviousSibling(); 594 while (n != null) { 595 if (n.getNodeType() == Node.TEXT_NODE 596 || n.getNodeType() == Node.CDATA_SECTION_NODE) { 597 count++; 598 } 599 n = n.getPreviousSibling(); 600 } 601 return count; 602 } 603 604 /** 605 * Get the relative position of this among same-target processing instruction siblings. 606 * @return 1..n 607 */ 608 private int getRelativePositionOfPI() { 609 int count = 1; 610 String target = ((ProcessingInstruction) node).getTarget(); 611 Node n = node.getPreviousSibling(); 612 while (n != null) { 613 if (n.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE 614 && ((ProcessingInstruction) n).getTarget().equals(target)) { 615 count++; 616 } 617 n = n.getPreviousSibling(); 618 } 619 return count; 620 } 621 622 public int hashCode() { 623 return node.hashCode(); 624 } 625 626 public boolean equals(Object object) { 627 return object == this || object instanceof DOMNodePointer && node == ((DOMNodePointer) object).node; 628 } 629 630 /** 631 * Get any prefix from the specified node. 632 * @param node the node to check 633 * @return String xml prefix 634 */ 635 public static String getPrefix(Node node) { 636 String prefix = node.getPrefix(); 637 if (prefix != null) { 638 return prefix; 639 } 640 641 String name = node.getNodeName(); 642 int index = name.lastIndexOf(':'); 643 return index < 0 ? null : name.substring(0, index); 644 } 645 646 /** 647 * Get the local name of the specified node. 648 * @param node node to check 649 * @return String local name 650 */ 651 public static String getLocalName(Node node) { 652 String localName = node.getLocalName(); 653 if (localName != null) { 654 return localName; 655 } 656 657 String name = node.getNodeName(); 658 int index = name.lastIndexOf(':'); 659 return index < 0 ? name : name.substring(index + 1); 660 } 661 662 /** 663 * Get the ns uri of the specified node. 664 * @param node Node to check 665 * @return String ns uri 666 */ 667 public static String getNamespaceURI(Node node) { 668 if (node instanceof Document) { 669 node = ((Document) node).getDocumentElement(); 670 } 671 672 Element element = (Element) node; 673 674 String uri = element.getNamespaceURI(); 675 if (uri != null) { 676 return uri; 677 } 678 679 String prefix = getPrefix(node); 680 String qname = prefix == null ? "xmlns" : "xmlns:" + prefix; 681 682 Node aNode = node; 683 while (aNode != null) { 684 if (aNode.getNodeType() == Node.ELEMENT_NODE) { 685 Attr attr = ((Element) aNode).getAttributeNode(qname); 686 if (attr != null) { 687 return attr.getValue(); 688 } 689 } 690 aNode = aNode.getParentNode(); 691 } 692 return null; 693 } 694 695 public Object getValue() { 696 if (node.getNodeType() == Node.COMMENT_NODE) { 697 String text = ((Comment) node).getData(); 698 return text == null ? "" : text.trim(); 699 } 700 return stringValue(node); 701 } 702 703 /** 704 * Get the string value of the specified node. 705 * @param node Node to check 706 * @return String 707 */ 708 private String stringValue(Node node) { 709 int nodeType = node.getNodeType(); 710 if (nodeType == Node.COMMENT_NODE) { 711 return ""; 712 } 713 boolean trim = !"preserve".equals(findEnclosingAttribute(node, "xml:space")); 714 if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) { 715 String text = node.getNodeValue(); 716 return text == null ? "" : trim ? text.trim() : text; 717 } 718 if (nodeType == Node.PROCESSING_INSTRUCTION_NODE) { 719 String text = ((ProcessingInstruction) node).getData(); 720 return text == null ? "" : trim ? text.trim() : text; 721 } 722 NodeList list = node.getChildNodes(); 723 StringBuffer buf = new StringBuffer(); 724 for (int i = 0; i < list.getLength(); i++) { 725 Node child = list.item(i); 726 buf.append(stringValue(child)); 727 } 728 return buf.toString(); 729 } 730 731 /** 732 * Locates a node by ID. 733 * @param context starting context 734 * @param id to find 735 * @return Pointer 736 */ 737 public Pointer getPointerByID(JXPathContext context, String id) { 738 Document document = node.getNodeType() == Node.DOCUMENT_NODE ? (Document) node 739 : node.getOwnerDocument(); 740 Element element = document.getElementById(id); 741 return element == null ? (Pointer) new NullPointer(getLocale(), id) 742 : new DOMNodePointer(element, getLocale(), id); 743 } 744 745 public int compareChildNodePointers(NodePointer pointer1, 746 NodePointer pointer2) { 747 Node node1 = (Node) pointer1.getBaseValue(); 748 Node node2 = (Node) pointer2.getBaseValue(); 749 if (node1 == node2) { 750 return 0; 751 } 752 753 int t1 = node1.getNodeType(); 754 int t2 = node2.getNodeType(); 755 if (t1 == Node.ATTRIBUTE_NODE && t2 != Node.ATTRIBUTE_NODE) { 756 return -1; 757 } 758 if (t1 != Node.ATTRIBUTE_NODE && t2 == Node.ATTRIBUTE_NODE) { 759 return 1; 760 } 761 if (t1 == Node.ATTRIBUTE_NODE && t2 == Node.ATTRIBUTE_NODE) { 762 NamedNodeMap map = ((Node) getNode()).getAttributes(); 763 int length = map.getLength(); 764 for (int i = 0; i < length; i++) { 765 Node n = map.item(i); 766 if (n == node1) { 767 return -1; 768 } 769 if (n == node2) { 770 return 1; 771 } 772 } 773 return 0; // Should not happen 774 } 775 776 Node current = node.getFirstChild(); 777 while (current != null) { 778 if (current == node1) { 779 return -1; 780 } 781 if (current == node2) { 782 return 1; 783 } 784 current = current.getNextSibling(); 785 } 786 return 0; 787 } 788 }