View Javadoc

1   /*
2    * Copyright 1999-2004 The Apache Software Foundation
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  package org.apache.commons.jxpath.ri.model;
17  
18  import java.util.Locale;
19  
20  import org.apache.commons.jxpath.JXPathContext;
21  import org.apache.commons.jxpath.JXPathException;
22  import org.apache.commons.jxpath.Pointer;
23  import org.apache.commons.jxpath.ri.Compiler;
24  import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
25  import org.apache.commons.jxpath.ri.NamespaceResolver;
26  import org.apache.commons.jxpath.ri.QName;
27  import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
28  import org.apache.commons.jxpath.ri.compiler.NodeTest;
29  import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
30  import org.apache.commons.jxpath.ri.model.beans.NullPointer;
31  
32  /***
33   * Common superclass for Pointers of all kinds.  A NodePointer maps to
34   * a deterministic XPath that represents the location of a node in an 
35   * object graph. This XPath uses only simple axes: child, namespace and
36   * attribute and only simple, context-independent predicates.
37   *
38   * @author Dmitri Plotnikov
39   * @version $Revision: 1.25 $ $Date: 2004/04/01 02:55:32 $
40   */
41  public abstract class NodePointer implements Pointer {
42  
43      public static final int WHOLE_COLLECTION = Integer.MIN_VALUE;
44      protected int index = WHOLE_COLLECTION;
45      public static final String UNKNOWN_NAMESPACE = "<<unknown namespace>>";
46      private boolean attribute = false;
47      private transient Object rootNode;
48      private NamespaceResolver namespaceResolver;
49      
50      /***
51       * Allocates an entirely new NodePointer by iterating through all installed
52       * NodePointerFactories until it finds one that can create a pointer.
53       */
54      public static NodePointer newNodePointer(
55          QName name,
56          Object bean,
57          Locale locale) 
58      {
59          NodePointer pointer = null;
60          if (bean == null) {
61              pointer = new NullPointer(name, locale);
62              return pointer;
63          }
64          
65          NodePointerFactory[] factories =
66              JXPathContextReferenceImpl.getNodePointerFactories();
67          for (int i = 0; i < factories.length; i++) {
68              pointer = factories[i].createNodePointer(name, bean, locale);
69              if (pointer != null) {
70                  return pointer;
71              }
72          }
73          throw new JXPathException(
74              "Could not allocate a NodePointer for object of "
75                  + bean.getClass());
76      }
77  
78      /***
79       * Allocates an new child NodePointer by iterating through all installed
80       * NodePointerFactories until it finds one that can create a pointer.
81       */
82      public static NodePointer newChildNodePointer(
83          NodePointer parent,
84          QName name,
85          Object bean) 
86      {
87          NodePointerFactory[] factories =
88              JXPathContextReferenceImpl.getNodePointerFactories();
89          for (int i = 0; i < factories.length; i++) {
90              NodePointer pointer =
91                  factories[i].createNodePointer(parent, name, bean);
92              if (pointer != null) {
93                  return pointer;
94              }
95          }
96          throw new JXPathException(
97              "Could not allocate a NodePointer for object of "
98                  + bean.getClass());
99      }
100 
101     protected NodePointer parent;
102     protected Locale locale;
103 //    private NamespaceManager namespaceManager;
104 
105     protected NodePointer(NodePointer parent) {
106         this.parent = parent;
107     }
108 
109     protected NodePointer(NodePointer parent, Locale locale) {
110         this.parent = parent;
111         this.locale = locale;
112     }
113 
114     public NamespaceResolver getNamespaceResolver() {
115         if (namespaceResolver == null && parent != null) {
116             namespaceResolver = parent.getNamespaceResolver();
117         }
118         return namespaceResolver;
119     }
120     
121     public void setNamespaceResolver(NamespaceResolver namespaceResolver) {
122         this.namespaceResolver = namespaceResolver;
123     }
124     
125     public NodePointer getParent() {
126         NodePointer pointer = parent;
127         while (pointer != null && pointer.isContainer()) {
128             pointer = pointer.getImmediateParentPointer();
129         }
130         return pointer;
131     }
132     
133     public NodePointer getImmediateParentPointer() {
134         return parent;
135     }
136 
137     /***
138      * Set to true if the pointer represents the "attribute::" axis.
139      */
140     public void setAttribute(boolean attribute) {
141         this.attribute = attribute;
142     }
143 
144     /***
145      * Returns true if the pointer represents the "attribute::" axis.
146      */
147     public boolean isAttribute() {
148         return attribute;
149     }
150 
151     /***
152      * Returns true if this Pointer has no parent.
153      */
154     public boolean isRoot() {
155         return parent == null;
156     }
157 
158     /***
159      * If true, this node does not have children
160      */
161     public abstract boolean isLeaf();
162 
163     /***
164      * @deprecated Please use !isContainer()
165      */
166     public boolean isNode() {
167         return !isContainer();
168     }
169      
170     /***
171      * If true, this node is axiliary and can only be used as an intermediate in
172      * the chain of pointers.
173      */
174     public boolean isContainer() {
175         return false;
176     }
177 
178     /***
179      * If the pointer represents a collection, the index identifies
180      * an element of that collection.  The default value of <code>index</code>
181      * is <code>WHOLE_COLLECTION</code>, which just means that the pointer
182      * is not indexed at all.
183      * Note: the index on NodePointer starts with 0, not 1.
184      */
185     public int getIndex() {
186         return index;
187     }
188 
189     public void setIndex(int index) {
190         this.index = index;
191     }
192 
193     /***
194      * Returns <code>true</code> if the value of the pointer is an array or
195      * a Collection.
196      */
197     public abstract boolean isCollection();
198 
199     /***
200      * If the pointer represents a collection (or collection element),
201      * returns the length of the collection.
202      * Otherwise returns 1 (even if the value is null).
203      */
204     public abstract int getLength();
205 
206     /***
207      * By default, returns <code>getNode()</code>, can be overridden to
208      * return a "canonical" value, like for instance a DOM element should
209      * return its string value.
210      */
211     public Object getValue() {
212         NodePointer valuePointer = getValuePointer();
213         if (valuePointer != this) {
214             return valuePointer.getValue();
215         }
216         // Default behavior is to return the same as getNode() 
217         return getNode();
218     }
219 
220     /***
221      * If this pointer manages a transparent container, like a variable,
222      * this method returns the pointer to the contents.
223      * Only an auxiliary (non-node) pointer can (and should) return a
224      * value pointer other than itself.
225      * Note that you probably don't want to override 
226      * <code>getValuePointer()</code> directly.  Override the
227      * <code>getImmediateValuePointer()</code> method instead.  The
228      * <code>getValuePointer()</code> method is calls
229      * <code>getImmediateValuePointer()</code> and, if the result is not
230      * <code>this</code>, invokes <code>getValuePointer()</code> recursively.
231      * The idea here is to open all nested containers. Let's say we have a
232      * container within a container within a container. The
233      * <code>getValuePointer()</code> method should then open all those
234      * containers and return the pointer to the ultimate contents. It does so
235      * with the above recursion.
236      */
237     public NodePointer getValuePointer() {
238         NodePointer ivp = getImmediateValuePointer();
239         if (ivp != this) {
240             return ivp.getValuePointer();
241         }
242         return this;
243     }
244 
245     /***
246      * @see #getValuePointer()
247      * 
248      * @return NodePointer is either <code>this</code> or a pointer
249      *   for the immediately contained value.
250      */
251     public NodePointer getImmediateValuePointer() {
252         return this;
253     }
254     
255     /***
256      * An actual pointer points to an existing part of an object graph, even
257      * if it is null. A non-actual pointer represents a part that does not exist
258      * at all.
259      * For instance consider the pointer "/address/street".
260      * If both <em>address</em> and <em>street</em> are not null,
261      * the pointer is actual.
262      * If <em>address</em> is not null, but <em>street</em> is null,
263      * the pointer is still actual.
264      * If <em>address</em> is null, the pointer is not actual.
265      * (In JavaBeans) if <em>address</em> is not a property of the root bean,
266      * a Pointer for this path cannot be obtained at all - actual or otherwise.
267      */
268     public boolean isActual() {
269         if (index == WHOLE_COLLECTION) {
270             return true;
271         }
272         else {
273             return index >= 0 && index < getLength();
274         }
275     }
276 
277     /***
278      * Returns the name of this node. Can be null.
279      */
280     public abstract QName getName();
281 
282     /***
283      * Returns the value represented by the pointer before indexing.
284      * So, if the node represents an element of a collection, this
285      * method returns the collection itself.
286      */
287     public abstract Object getBaseValue();
288 
289     /***
290      * Returns the object the pointer points to; does not convert it
291      * to a "canonical" type.
292      *
293      * @deprecated 1.1 Please use getNode()
294      */
295     public Object getNodeValue() {
296         return getNode();
297     }
298 
299     /***
300      * Returns the object the pointer points to; does not convert it
301      * to a "canonical" type. Opens containers, properties etc and returns
302      * the ultimate contents.
303      */
304     public Object getNode() {
305         return getValuePointer().getImmediateNode();
306     }
307     
308     public Object getRootNode() {
309         if (rootNode == null) {
310             if (parent != null) {
311                 rootNode = parent.getRootNode();
312             }
313             else {
314                 rootNode = getImmediateNode();
315             }
316         }
317         return rootNode;
318     }
319     
320     /***
321      * Returns the object the pointer points to; does not convert it
322      * to a "canonical" type.
323      */
324     public abstract Object getImmediateNode();
325 
326     /***
327      * Converts the value to the required type and changes the corresponding
328      * object to that value.
329      */
330     public abstract void setValue(Object value);
331 
332     /***
333      * Compares two child NodePointers and returns a positive number,
334      * zero or a positive number according to the order of the pointers.
335      */
336     public abstract int compareChildNodePointers(
337             NodePointer pointer1, NodePointer pointer2);
338 
339     /***
340      * Checks if this Pointer matches the supplied NodeTest.
341      */
342     public boolean testNode(NodeTest test) {
343         if (test == null) {
344             return true;
345         }
346         else if (test instanceof NodeNameTest) {
347             if (isContainer()) {
348                 return false;
349             }
350             NodeNameTest nodeNameTest = (NodeNameTest) test;
351             QName testName = nodeNameTest.getNodeName();
352             QName nodeName = getName();
353             if (nodeName == null) {
354                 return false;
355             }
356             
357             String testPrefix = testName.getPrefix();
358             String nodePrefix = nodeName.getPrefix();
359             if (!equalStrings(testPrefix, nodePrefix)) {
360                 String testNS = getNamespaceURI(testPrefix);
361                 String nodeNS = getNamespaceURI(nodePrefix);
362                 if (!equalStrings(testNS, nodeNS)) {
363                     return false;
364                 }
365             }
366             if (nodeNameTest.isWildcard()) {
367                 return true;
368             }
369             return testName.getName().equals(nodeName.getName());
370         }
371         else if (test instanceof NodeTypeTest) {
372             if (((NodeTypeTest) test).getNodeType()
373                 == Compiler.NODE_TYPE_NODE) {
374                 return isNode();
375             }
376         }
377         return false;
378     }
379 
380     private static boolean equalStrings(String s1, String s2) {
381         if (s1 == null && s2 != null) {
382             return false;
383         }
384         if (s1 != null && !s1.equals(s2)) {
385             return false;
386         }
387         return true;
388     }
389 
390     /***
391      *  Called directly by JXPathContext. Must create path and
392      *  set value.
393      */
394     public NodePointer createPath(JXPathContext context, Object value) {
395         setValue(value);
396         return this;
397     }
398 
399     /***
400      * Remove the node of the object graph this pointer points to.
401      */
402     public void remove() {
403         // It is a no-op
404 
405 //        System.err.println("REMOVING: " + asPath() + " " + getClass());
406 //        printPointerChain();
407     }
408 
409     /***
410      * Called by a child pointer when it needs to create a parent object.
411      * Must create an object described by this pointer and return
412      * a new pointer that properly describes the new object.
413      */
414     public NodePointer createPath(JXPathContext context) {
415         return this;
416     }
417 
418     /***
419      * Called by a child pointer if that child needs to assign the value
420      * supplied in the createPath(context, value) call to a non-existent
421      * node. This method may have to expand the collection in order to assign
422      * the element.
423      */
424     public NodePointer createChild(
425         JXPathContext context,
426         QName name,
427         int index,
428         Object value) 
429     {
430         throw new JXPathException(
431             "Cannot create an object for path "
432                 + asPath()
433                 + "/"
434                 + name
435                 + "["
436                 + (index + 1)
437                 + "]"
438                 + ", operation is not allowed for this type of node");
439     }
440 
441     /***
442      * Called by a child pointer when it needs to create a parent object
443      * for a non-existent collection element.  It may have to expand the
444      * collection, then create an element object and return a new pointer
445      * describing the newly created element.
446      */
447     public NodePointer createChild(
448         JXPathContext context,
449         QName name,
450         int index) 
451     {
452         throw new JXPathException(
453             "Cannot create an object for path "
454                 + asPath()
455                 + "/"
456                 + name
457                 + "["
458                 + (index + 1)
459                 + "]"
460                 + ", operation is not allowed for this type of node");
461     }
462     
463     /***
464      * Called to create a non-existing attribute
465      */
466     public NodePointer createAttribute(JXPathContext context, QName name) {
467         throw new JXPathException(
468             "Cannot create an attribute for path "
469                 + asPath() + "/@" + name
470                 + ", operation is not allowed for this type of node");
471     }
472 
473     /***
474      * If the Pointer has a parent, returns the parent's locale;
475      * otherwise returns the locale specified when this Pointer
476      * was created.
477      */
478     public Locale getLocale() {
479         if (locale == null) {
480             if (parent != null) {
481                 locale = parent.getLocale();
482             }
483         }
484         return locale;
485     }
486 
487     /***
488      * Returns true if the selected locale name starts
489      * with the specified prefix <i>lang</i>, case-insensitive.
490      */
491     public boolean isLanguage(String lang) {
492         Locale loc = getLocale();
493         String name = loc.toString().replace('_', '-');
494         return name.toUpperCase().startsWith(lang.toUpperCase());
495     }
496 
497 //    /***
498 //     * Installs the supplied manager as the namespace manager for this node
499 //     * pointer. The {@link #getNamespaceURI(String) getNamespaceURI(prefix)}
500 //     * uses this manager to resolve namespace prefixes.
501 //     * 
502 //     * @param namespaceManager
503 //     */
504 //    public void setNamespaceManager(NamespaceManager namespaceManager) {
505 //        this.namespaceManager = namespaceManager;
506 //    }
507 //    
508 //    public NamespaceManager getNamespaceManager() {
509 //        if (namespaceManager != null) {
510 //            return namespaceManager;
511 //        }
512 //        if (parent != null) {
513 //            return parent.getNamespaceManager();
514 //        }        
515 //        return null;
516 //    }
517 //    
518     /***
519      * Returns a NodeIterator that iterates over all children or all children
520      * that match the given NodeTest, starting with the specified one.
521      */
522     public NodeIterator childIterator(
523         NodeTest test,
524         boolean reverse,
525         NodePointer startWith) 
526     {
527         NodePointer valuePointer = getValuePointer();
528         if (valuePointer != null && valuePointer != this) {
529             return valuePointer.childIterator(test, reverse, startWith);
530         }
531         return null;
532     }
533 
534     /***
535      * Returns a NodeIterator that iterates over all attributes of the current
536      * node matching the supplied node name (could have a wildcard).
537      * May return null if the object does not support the attributes.
538      */
539     public NodeIterator attributeIterator(QName qname) {
540         NodePointer valuePointer = getValuePointer();
541         if (valuePointer != null && valuePointer != this) {
542             return valuePointer.attributeIterator(qname);
543         }
544         return null;
545     }
546 
547     /***
548      * Returns a NodeIterator that iterates over all namespaces of the value
549      * currently pointed at.
550      * May return null if the object does not support the namespaces.
551      */
552     public NodeIterator namespaceIterator() {
553         return null;
554     }
555 
556     /***
557      * Returns a NodePointer for the specified namespace. Will return null
558      * if namespaces are not supported.
559      * Will return UNKNOWN_NAMESPACE if there is no such namespace.
560      */
561     public NodePointer namespacePointer(String namespace) {
562         return null;
563     }
564 
565     /***
566      * Decodes a namespace prefix to the corresponding URI.
567      */
568     public String getNamespaceURI(String prefix) {
569         return null;
570     }
571 
572     /***
573      * Returns the namespace URI associated with this Pointer.
574      */
575     public String getNamespaceURI() {
576         return null;
577     }
578 
579     /***
580      * Returns true if the supplied prefix represents the
581      * default namespace in the context of the current node.
582      */
583     protected boolean isDefaultNamespace(String prefix) {
584         if (prefix == null) {
585             return true;
586         }
587 
588         String namespace = getNamespaceURI(prefix);
589         if (namespace == null) {
590             return false; // undefined namespace
591         }
592 
593         return namespace.equals(getDefaultNamespaceURI());
594     }
595 
596     protected String getDefaultNamespaceURI() {
597         return null;
598     }
599 
600     /***
601      * Locates a node by ID.
602      */
603     public Pointer getPointerByID(JXPathContext context, String id) {
604         return context.getPointerByID(id);
605     }
606 
607     /***
608      * Locates a node by key and value.
609      */
610     public Pointer getPointerByKey(
611         JXPathContext context,
612         String key,
613         String value) 
614     {
615         return context.getPointerByKey(key, value);
616     }
617 
618     /***
619      * Returns an XPath that maps to this Pointer.
620      */
621     public String asPath() {
622         // If the parent of this node is a container, it is responsible
623         // for appended this node's part of the path.
624         if (parent != null && parent.isContainer()) {
625             return parent.asPath();
626         }
627 
628         StringBuffer buffer = new StringBuffer();
629         if (parent != null) {
630             buffer.append(parent.asPath());
631         }
632 
633         if (buffer.length() == 0
634             || buffer.charAt(buffer.length() - 1) != '/') {
635             buffer.append('/');
636         }
637         if (attribute) {
638             buffer.append('@');
639         }
640         buffer.append(getName());
641 
642         if (index != WHOLE_COLLECTION && isCollection()) {
643             buffer.append('[').append(index + 1).append(']');
644         }
645         return buffer.toString();
646     }
647 
648     public Object clone() {
649         try {
650             NodePointer ptr = (NodePointer) super.clone();
651             if (parent != null) {
652                 ptr.parent = (NodePointer) parent.clone();
653             }
654             return ptr;
655         }
656         catch (CloneNotSupportedException ex) {
657             // Of course it is supported
658             ex.printStackTrace();
659         }
660         return null;
661     }
662 
663     public String toString() {
664         return asPath();
665     }
666 
667     public int compareTo(Object object) {
668         // Let it throw a ClassCastException
669         NodePointer pointer = (NodePointer) object;
670         if (parent == pointer.parent) {
671             if (parent == null) {
672                 return 0;
673             }
674             return parent.compareChildNodePointers(this, pointer);
675         }
676 
677         // Task 1: find the common parent
678         int depth1 = 0;
679         NodePointer p1 = this;
680         while (p1 != null) {
681             depth1++;
682             p1 = p1.parent;
683         }
684         int depth2 = 0;
685         NodePointer p2 = pointer;
686         while (p2 != null) {
687             depth2++;
688             p2 = p2.parent;
689         }
690         return compareNodePointers(this, depth1, pointer, depth2);
691     }
692 
693     private int compareNodePointers(
694         NodePointer p1,
695         int depth1,
696         NodePointer p2,
697         int depth2) 
698     {
699         if (depth1 < depth2) {
700             int r = compareNodePointers(p1, depth1, p2.parent, depth2 - 1);
701             if (r != 0) {
702                 return r;
703             }
704             return -1;
705         }
706         else if (depth1 > depth2) {
707             int r = compareNodePointers(p1.parent, depth1 - 1, p2, depth2);
708             if (r != 0) {
709                 return r;
710             }
711             return 1;
712         }
713         if (p1 == null && p2 == null) {
714             return 0;
715         }
716 
717         if (p1 != null && p1.equals(p2)) {
718             return 0;
719         }
720 
721         if (depth1 == 1) {
722             throw new JXPathException(
723                 "Cannot compare pointers that do not belong to the same tree: '"
724                     + p1
725                     + "' and '"
726                     + p2
727                     + "'");
728         }
729         int r =
730             compareNodePointers(p1.parent, depth1 - 1, p2.parent, depth2 - 1);
731         if (r != 0) {
732             return r;
733         }
734 
735         return p1.parent.compareChildNodePointers(p1, p2);
736     }
737 
738     /***
739      * Print internal structure of a pointer for debugging
740      */
741     public void printPointerChain() {
742         printDeep(this, "");
743     }
744 
745     private static void printDeep(NodePointer pointer, String indent) {
746         if (indent.length() == 0) {
747             System.err.println(
748                 "POINTER: "
749                     + pointer
750                     + "("
751                     + pointer.getClass().getName()
752                     + ")");
753         }
754         else {
755             System.err.println(
756                 indent
757                     + " of "
758                     + pointer
759                     + "("
760                     + pointer.getClass().getName()
761                     + ")");
762         }
763         if (pointer.getImmediateParentPointer() != null) {
764             printDeep(pointer.getImmediateParentPointer(), indent + "  ");
765         }
766     }
767 }