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 }