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 018package org.apache.commons.jxpath.ri.model.jdom; 019 020import java.util.List; 021import java.util.Locale; 022 023import org.apache.commons.jxpath.JXPathAbstractFactoryException; 024import org.apache.commons.jxpath.JXPathContext; 025import org.apache.commons.jxpath.JXPathException; 026import org.apache.commons.jxpath.ri.Compiler; 027import org.apache.commons.jxpath.ri.NamespaceResolver; 028import org.apache.commons.jxpath.ri.QName; 029import org.apache.commons.jxpath.ri.compiler.NodeNameTest; 030import org.apache.commons.jxpath.ri.compiler.NodeTest; 031import org.apache.commons.jxpath.ri.compiler.NodeTypeTest; 032import org.apache.commons.jxpath.ri.compiler.ProcessingInstructionTest; 033import org.apache.commons.jxpath.ri.model.NodeIterator; 034import org.apache.commons.jxpath.ri.model.NodePointer; 035import org.apache.commons.jxpath.util.TypeUtils; 036import org.jdom.Attribute; 037import org.jdom.CDATA; 038import org.jdom.Comment; 039import org.jdom.Document; 040import org.jdom.Element; 041import org.jdom.Namespace; 042import org.jdom.ProcessingInstruction; 043import org.jdom.Text; 044 045/** 046 * A Pointer that points to a DOM node. 047 */ 048public class JDOMNodePointer extends NodePointer { 049 050 private static final long serialVersionUID = -6346532297491082651L; 051 /** XML ns uri */ 052 public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace"; 053 /** XMLNS ns uri */ 054 public static final String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/"; 055 056 /** 057 * Tests whether two strings are == or .equals() 058 * 059 * @param s1 string 1 060 * @param s2 string 2 061 * @return true if equal 062 */ 063 private static boolean equalStrings(String s1, String s2) { 064 if (s1 == s2) { 065 return true; 066 } 067 s1 = s1 == null ? "" : s1.trim(); 068 s2 = s2 == null ? "" : s2.trim(); 069 return s1.equals(s2); 070 } 071 072 /** 073 * Find the nearest occurrence of the specified attribute on the specified and enclosing elements. 074 * 075 * @param n current node 076 * @param attrName attribute name 077 * @param ns Namespace 078 * @return attribute value 079 */ 080 protected static String findEnclosingAttribute(Object n, final String attrName, final Namespace ns) { 081 while (n != null) { 082 if (n instanceof Element) { 083 final Element e = (Element) n; 084 final String attr = e.getAttributeValue(attrName, ns); 085 if (attr != null && !attr.isEmpty()) { 086 return attr; 087 } 088 } 089 n = nodeParent(n); 090 } 091 return null; 092 } 093 094 /** 095 * Gets the local name of the specified node. 096 * 097 * @param node to check 098 * @return String local name 099 */ 100 public static String getLocalName(final Object node) { 101 if (node instanceof Element) { 102 return ((Element) node).getName(); 103 } 104 if (node instanceof Attribute) { 105 return ((Attribute) node).getName(); 106 } 107 return null; 108 } 109 110 /** 111 * Gets the ns uri of the specified node. 112 * 113 * @param node Node to check 114 * @return String 115 */ 116 private static String getNamespaceURI(final Object node) { 117 if (node instanceof Element) { 118 final Element element = (Element) node; 119 String ns = element.getNamespaceURI(); 120 if ("".equals(ns)) { 121 ns = null; 122 } 123 return ns; 124 } 125 return null; 126 } 127 128 /** 129 * Gets the prefix from a given node. 130 * 131 * @param node to check 132 * @return String 133 */ 134 public static String getPrefix(final Object node) { 135 if (node instanceof Element) { 136 final String prefix = ((Element) node).getNamespacePrefix(); 137 return prefix == null || prefix.isEmpty() ? null : prefix; 138 } 139 if (node instanceof Attribute) { 140 final String prefix = ((Attribute) node).getNamespacePrefix(); 141 return prefix == null || prefix.isEmpty() ? null : prefix; 142 } 143 return null; 144 } 145 146 /** 147 * Gets the parent of the specified node. 148 * 149 * @param node to check 150 * @return parent Element 151 */ 152 private static Element nodeParent(final Object node) { 153 if (node instanceof Element) { 154 final Object parent = ((Element) node).getParent(); 155 return parent instanceof Element ? (Element) parent : null; 156 } 157 if (node instanceof Text) { 158 return (Element) ((Text) node).getParent(); 159 } 160 if (node instanceof CDATA) { 161 return (Element) ((CDATA) node).getParent(); 162 } 163 if (node instanceof ProcessingInstruction) { 164 return (Element) ((ProcessingInstruction) node).getParent(); 165 } 166 if (node instanceof Comment) { 167 return (Element) ((Comment) node).getParent(); 168 } 169 return null; 170 } 171 172 /** 173 * Execute test against node on behalf of pointer. 174 * 175 * @param pointer Pointer 176 * @param node to test 177 * @param test to execute 178 * @return true if node passes test 179 */ 180 public static boolean testNode(final NodePointer pointer, final Object node, final NodeTest test) { 181 if (test == null) { 182 return true; 183 } 184 if (test instanceof NodeNameTest) { 185 if (!(node instanceof Element)) { 186 return false; 187 } 188 final NodeNameTest nodeNameTest = (NodeNameTest) test; 189 final QName testName = nodeNameTest.getNodeName(); 190 final String namespaceURI = nodeNameTest.getNamespaceURI(); 191 final boolean wildcard = nodeNameTest.isWildcard(); 192 final String testPrefix = testName.getPrefix(); 193 if (wildcard && testPrefix == null) { 194 return true; 195 } 196 if (wildcard || testName.getName().equals(getLocalName(node))) { 197 final String nodeNS = getNamespaceURI(node); 198 return equalStrings(namespaceURI, nodeNS) || nodeNS == null && equalStrings(testPrefix, getPrefix(node)); 199 } 200 return false; 201 } 202 if (test instanceof NodeTypeTest) { 203 switch (((NodeTypeTest) test).getNodeType()) { 204 case Compiler.NODE_TYPE_NODE: 205 return true; 206 case Compiler.NODE_TYPE_TEXT: 207 return node instanceof Text || node instanceof CDATA; 208 case Compiler.NODE_TYPE_COMMENT: 209 return node instanceof Comment; 210 case Compiler.NODE_TYPE_PI: 211 return node instanceof ProcessingInstruction; 212 default: 213 return false; 214 } 215 } 216 if (test instanceof ProcessingInstructionTest && node instanceof ProcessingInstruction) { 217 final String testPI = ((ProcessingInstructionTest) test).getTarget(); 218 final String nodePI = ((ProcessingInstruction) node).getTarget(); 219 return testPI.equals(nodePI); 220 } 221 return false; 222 } 223 224 /** Node, like a bean. */ 225 private final Object node; 226 227 /** Optional ID, may be null. */ 228 private final String id; 229 230 /** Local namespace resolver. */ 231 private NamespaceResolver localNamespaceResolver; 232 233 /** 234 * Constructs a new JDOMNodePointer. 235 * 236 * @param parent NodePointer 237 * @param node pointed 238 */ 239 public JDOMNodePointer(final NodePointer parent, final Object node) { 240 super(parent); 241 this.node = node; 242 this.id = null; 243 } 244 245 /** 246 * Constructs a new JDOMNodePointer. 247 * 248 * @param node node, like a bean. 249 * @param locale Locale 250 */ 251 public JDOMNodePointer(final Object node, final Locale locale) { 252 super(null, locale); 253 this.node = node; 254 this.id = null; 255 } 256 257 /** 258 * Constructs a new JDOMNodePointer. 259 * 260 * @param node pointed 261 * @param locale Locale 262 * @param id String id 263 */ 264 public JDOMNodePointer(final Object node, final Locale locale, final String id) { 265 super(null, locale); 266 this.node = node; 267 this.id = id; 268 } 269 270 /** 271 * Add the specified content to this element. 272 * 273 * @param content List 274 */ 275 private void addContent(final List content) { 276 final Element element = (Element) node; 277 final int count = content.size(); 278 for (int i = 0; i < count; i++) { 279 Object child = content.get(i); 280 if (child instanceof Element) { 281 child = ((Element) child).clone(); 282 element.addContent((Element) child); 283 } else if (child instanceof Text) { 284 child = ((Text) child).clone(); 285 element.addContent((Text) child); 286 } else if (node instanceof CDATA) { 287 child = ((CDATA) child).clone(); 288 element.addContent((CDATA) child); 289 } else if (node instanceof ProcessingInstruction) { 290 child = ((ProcessingInstruction) child).clone(); 291 element.addContent((ProcessingInstruction) child); 292 } else if (node instanceof Comment) { 293 child = ((Comment) child).clone(); 294 element.addContent((Comment) child); 295 } 296 } 297 } 298 299 @Override 300 public String asPath() { 301 if (id != null) { 302 return "id('" + escape(id) + "')"; 303 } 304 final StringBuilder buffer = new StringBuilder(); 305 if (parent != null) { 306 buffer.append(parent.asPath()); 307 } 308 if (node instanceof Element) { 309 // If the parent pointer is not a JDOMNodePointer, it is 310 // the parent's responsibility to produce the node test part 311 // of the path 312 if (parent instanceof JDOMNodePointer) { 313 if (buffer.length() == 0 || buffer.charAt(buffer.length() - 1) != '/') { 314 buffer.append('/'); 315 } 316 final String nsURI = getNamespaceURI(); 317 final String ln = getLocalName(node); 318 if (nsURI == null) { 319 buffer.append(ln); 320 buffer.append('['); 321 buffer.append(getRelativePositionByQName()).append(']'); 322 } else { 323 final String prefix = getNamespaceResolver().getPrefix(nsURI); 324 if (prefix != null) { 325 buffer.append(prefix); 326 buffer.append(':'); 327 buffer.append(ln); 328 buffer.append('['); 329 buffer.append(getRelativePositionByQName()); 330 } else { 331 buffer.append("node()"); 332 buffer.append('['); 333 buffer.append(getRelativePositionOfElement()); 334 } 335 buffer.append(']'); 336 } 337 } 338 } else if (node instanceof Text || node instanceof CDATA) { 339 buffer.append("/text()"); 340 buffer.append('[').append(getRelativePositionOfTextNode()).append(']'); 341 } else if (node instanceof ProcessingInstruction) { 342 buffer.append("/processing-instruction(\'").append(((ProcessingInstruction) node).getTarget()).append("')"); 343 buffer.append('[').append(getRelativePositionOfPI()).append(']'); 344 } 345 return buffer.toString(); 346 } 347 348 @Override 349 public NodeIterator attributeIterator(final QName qName) { 350 return new JDOMAttributeIterator(this, qName); 351 } 352 353 @Override 354 public NodeIterator childIterator(final NodeTest test, final boolean reverse, final NodePointer startWith) { 355 return new JDOMNodeIterator(this, test, reverse, startWith); 356 } 357 358 @Override 359 public int compareChildNodePointers(final NodePointer pointer1, final NodePointer pointer2) { 360 final Object node1 = pointer1.getBaseValue(); 361 final Object node2 = pointer2.getBaseValue(); 362 if (node1 == node2) { 363 return 0; 364 } 365 if (node1 instanceof Attribute && !(node2 instanceof Attribute)) { 366 return -1; 367 } 368 if (!(node1 instanceof Attribute) && node2 instanceof Attribute) { 369 return 1; 370 } 371 if (node1 instanceof Attribute && node2 instanceof Attribute) { 372 final List list = ((Element) getNode()).getAttributes(); 373 final int length = list.size(); 374 for (int i = 0; i < length; i++) { 375 final Object n = list.get(i); 376 if (n == node1) { 377 return -1; 378 } 379 if (n == node2) { 380 return 1; 381 } 382 } 383 return 0; // Should not happen 384 } 385 if (!(node instanceof Element)) { 386 throw new IllegalStateException("JXPath internal error: compareChildNodes called for " + node); 387 } 388 final List children = ((Element) node).getContent(); 389 final int length = children.size(); 390 for (int i = 0; i < length; i++) { 391 final Object n = children.get(i); 392 if (n == node1) { 393 return -1; 394 } 395 if (n == node2) { 396 return 1; 397 } 398 } 399 return 0; 400 } 401 402 @Override 403 public NodePointer createAttribute(final JXPathContext context, final QName qName) { 404 if (!(node instanceof Element)) { 405 return super.createAttribute(context, qName); 406 } 407 final Element element = (Element) node; 408 final String prefix = qName.getPrefix(); 409 if (prefix != null) { 410 final String namespaceUri = getNamespaceResolver().getNamespaceURI(prefix); 411 if (namespaceUri == null) { 412 throw new JXPathException("Unknown namespace prefix: " + prefix); 413 } 414 final Namespace ns = Namespace.getNamespace(prefix, namespaceUri); 415 final Attribute attr = element.getAttribute(qName.getName(), ns); 416 if (attr == null) { 417 element.setAttribute(qName.getName(), "", ns); 418 } 419 } else { 420 final Attribute attr = element.getAttribute(qName.getName()); 421 if (attr == null) { 422 element.setAttribute(qName.getName(), ""); 423 } 424 } 425 final NodeIterator it = attributeIterator(qName); 426 it.setPosition(1); 427 return it.getNodePointer(); 428 } 429 430 @Override 431 public NodePointer createChild(final JXPathContext context, final QName qName, int index) { 432 if (index == WHOLE_COLLECTION) { 433 index = 0; 434 } 435 final boolean success = getAbstractFactory(context).createObject(context, this, node, qName.toString(), index); 436 if (success) { 437 NodeTest nodeTest; 438 final String prefix = qName.getPrefix(); 439 final String namespaceURI = prefix == null ? null : context.getNamespaceURI(prefix); 440 nodeTest = new NodeNameTest(qName, namespaceURI); 441 final NodeIterator it = childIterator(nodeTest, false, null); 442 if (it != null && it.setPosition(index + 1)) { 443 return it.getNodePointer(); 444 } 445 } 446 throw new JXPathAbstractFactoryException("Factory could not create a child node for path: " + asPath() + "/" + qName + "[" + (index + 1) + "]"); 447 } 448 449 @Override 450 public NodePointer createChild(final JXPathContext context, final QName qName, final int index, final Object value) { 451 final NodePointer ptr = createChild(context, qName, index); 452 ptr.setValue(value); 453 return ptr; 454 } 455 456 @Override 457 public boolean equals(final Object object) { 458 if (object == this) { 459 return true; 460 } 461 if (!(object instanceof JDOMNodePointer)) { 462 return false; 463 } 464 final JDOMNodePointer other = (JDOMNodePointer) object; 465 return node == other.node; 466 } 467 468 @Override 469 public Object getBaseValue() { 470 return node; 471 } 472 473 @Override 474 public Object getImmediateNode() { 475 return node; 476 } 477 478 /** 479 * Gets the language of this element. 480 * 481 * @return String language 482 */ 483 protected String getLanguage() { 484 return findEnclosingAttribute(node, "lang", Namespace.XML_NAMESPACE); 485 } 486 487 @Override 488 public int getLength() { 489 return 1; 490 } 491 492 @Override 493 public QName getName() { 494 String ns = null; 495 String ln = null; 496 if (node instanceof Element) { 497 ns = ((Element) node).getNamespacePrefix(); 498 if (ns != null && ns.isEmpty()) { 499 ns = null; 500 } 501 ln = ((Element) node).getName(); 502 } else if (node instanceof ProcessingInstruction) { 503 ln = ((ProcessingInstruction) node).getTarget(); 504 } 505 return new QName(ns, ln); 506 } 507 508 @Override 509 public synchronized NamespaceResolver getNamespaceResolver() { 510 if (localNamespaceResolver == null) { 511 localNamespaceResolver = new NamespaceResolver(super.getNamespaceResolver()); 512 localNamespaceResolver.setNamespaceContextPointer(this); 513 } 514 return localNamespaceResolver; 515 } 516 517 @Override 518 public String getNamespaceURI() { 519 return getNamespaceURI(node); 520 } 521 522 @Override 523 public String getNamespaceURI(final String prefix) { 524 if (prefix.equals("xml")) { 525 return Namespace.XML_NAMESPACE.getURI(); 526 } 527 Element element = null; 528 if (node instanceof Document) { 529 element = ((Document) node).getRootElement(); 530 } 531 if (node instanceof Element) { 532 element = (Element) node; 533 } 534 if (element == null) { 535 return null; 536 } 537 final Namespace ns = element.getNamespace(prefix); 538 return ns == null ? null : ns.getURI(); 539 } 540 541 /** 542 * Gets relative position of this among like-named siblings. 543 * 544 * @return 1..n 545 */ 546 private int getRelativePositionByQName() { 547 if (node instanceof Element) { 548 final Object parent = ((Element) node).getParent(); 549 if (!(parent instanceof Element)) { 550 return 1; 551 } 552 final List children = ((Element) parent).getContent(); 553 int count = 0; 554 for (final Object child : children) { 555 if (child instanceof Element && matchesQName((Element) child)) { 556 count++; 557 } 558 if (child == node) { 559 break; 560 } 561 } 562 return count; 563 } 564 return 1; 565 } 566 567 /** 568 * Gets relative position of this among all siblings. 569 * 570 * @return 1..n 571 */ 572 private int getRelativePositionOfElement() { 573 final Object parent = ((Element) node).getParent(); 574 if (parent == null) { 575 return 1; 576 } 577 List children; 578 if (parent instanceof Element) { 579 children = ((Element) parent).getContent(); 580 } else { 581 children = ((Document) parent).getContent(); 582 } 583 int count = 0; 584 for (final Object child : children) { 585 if (child instanceof Element) { 586 count++; 587 } 588 if (child == node) { 589 break; 590 } 591 } 592 return count; 593 } 594 595 /** 596 * Gets the relative position of this among same-target processing instruction siblings. 597 * 598 * @return 1..n 599 */ 600 private int getRelativePositionOfPI() { 601 final String target = ((ProcessingInstruction) node).getTarget(); 602 final Element parent = (Element) ((ProcessingInstruction) node).getParent(); 603 if (parent == null) { 604 return 1; 605 } 606 final List children = parent.getContent(); 607 int count = 0; 608 for (final Object child : children) { 609 if (child instanceof ProcessingInstruction && (target == null || target.equals(((ProcessingInstruction) child).getTarget()))) { 610 count++; 611 } 612 if (child == node) { 613 break; 614 } 615 } 616 return count; 617 } 618 619 /** 620 * Gets the relative position of this among sibling text nodes. 621 * 622 * @return 1..n 623 */ 624 private int getRelativePositionOfTextNode() { 625 Element parent; 626 if (node instanceof Text) { 627 parent = (Element) ((Text) node).getParent(); 628 } else { 629 parent = (Element) ((CDATA) node).getParent(); 630 } 631 if (parent == null) { 632 return 1; 633 } 634 final List children = parent.getContent(); 635 int count = 0; 636 for (final Object child : children) { 637 if (child instanceof Text || child instanceof CDATA) { 638 count++; 639 } 640 if (child == node) { 641 break; 642 } 643 } 644 return count; 645 } 646 647 @Override 648 public Object getValue() { 649 if (node instanceof Element) { 650 final StringBuilder buf = new StringBuilder(); 651 for (final NodeIterator children = childIterator(null, false, null); children.setPosition(children.getPosition() + 1);) { 652 final NodePointer ptr = children.getNodePointer(); 653 if (ptr.getImmediateNode() instanceof Element || ptr.getImmediateNode() instanceof Text) { 654 buf.append(ptr.getValue()); 655 } 656 } 657 return buf.toString(); 658 } 659 if (node instanceof Comment) { 660 String text = ((Comment) node).getText(); 661 if (text != null) { 662 text = text.trim(); 663 } 664 return text; 665 } 666 String result = null; 667 if (node instanceof Text) { 668 result = ((Text) node).getText(); 669 } 670 if (node instanceof ProcessingInstruction) { 671 result = ((ProcessingInstruction) node).getData(); 672 } 673 final boolean trim = !"preserve".equals(findEnclosingAttribute(node, "space", Namespace.XML_NAMESPACE)); 674 return result != null && trim ? result.trim() : result; 675 } 676 677 @Override 678 public int hashCode() { 679 return node.hashCode(); 680 } 681 682 @Override 683 public boolean isCollection() { 684 return false; 685 } 686 687 /** 688 * Returns true if the xml:lang attribute for the current node or its parent has the specified prefix <em>lang</em>. If no node has this prefix, calls 689 * {@code super.isLanguage(lang)}. 690 * 691 * @param lang to compare 692 * @return true if this element uses the specified language. 693 */ 694 @Override 695 public boolean isLanguage(final String lang) { 696 final String current = getLanguage(); 697 return current == null ? super.isLanguage(lang) : current.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH)); 698 } 699 700 @Override 701 public boolean isLeaf() { 702 if (node instanceof Element) { 703 return ((Element) node).getContent().isEmpty(); 704 } 705 if (node instanceof Document) { 706 return ((Document) node).getContent().isEmpty(); 707 } 708 return true; 709 } 710 711 private boolean matchesQName(final Element element) { 712 if (getNamespaceURI() != null) { 713 final String ns = getNamespaceURI(element); 714 if (ns == null || !ns.equals(getNamespaceURI())) { 715 return false; 716 } 717 } 718 return element.getName().equals(((Element) node).getName()); 719 } 720 721 @Override 722 public NodeIterator namespaceIterator() { 723 return new JDOMNamespaceIterator(this); 724 } 725 726 @Override 727 public NodePointer namespacePointer(final String prefix) { 728 return new JDOMNamespacePointer(this, prefix); 729 } 730 731 @Override 732 public void remove() { 733 final Element parent = nodeParent(node); 734 if (parent == null) { 735 throw new JXPathException("Cannot remove root JDOM node"); 736 } 737 parent.getContent().remove(node); 738 } 739 740 @Override 741 public void setValue(final Object value) { 742 if (node instanceof Text) { 743 final String string = (String) TypeUtils.convert(value, String.class); 744 if (string != null && !string.isEmpty()) { 745 ((Text) node).setText(string); 746 } else { 747 nodeParent(node).removeContent((Text) node); 748 } 749 } else { 750 final Element element = (Element) node; 751 element.getContent().clear(); 752 if (value instanceof Element) { 753 final Element valueElement = (Element) value; 754 addContent(valueElement.getContent()); 755 } else if (value instanceof Document) { 756 final Document valueDocument = (Document) value; 757 addContent(valueDocument.getContent()); 758 } else if (value instanceof Text || value instanceof CDATA) { 759 final String string = ((Text) value).getText(); 760 element.addContent(new Text(string)); 761 } else if (value instanceof ProcessingInstruction) { 762 final ProcessingInstruction pi = (ProcessingInstruction) ((ProcessingInstruction) value).clone(); 763 element.addContent(pi); 764 } else if (value instanceof Comment) { 765 final Comment comment = (Comment) ((Comment) value).clone(); 766 element.addContent(comment); 767 } else { 768 final String string = (String) TypeUtils.convert(value, String.class); 769 if (string != null && !string.isEmpty()) { 770 element.addContent(new Text(string)); 771 } 772 } 773 } 774 } 775 776 @Override 777 public boolean testNode(final NodeTest test) { 778 return testNode(this, node, test); 779 } 780}