001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements. See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership. The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the  "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    /*
019     * $Id: XPathResultImpl.java 1225426 2011-12-29 04:13:08Z mrglavas $
020     */
021    
022    
023    package org.apache.xpath.domapi;
024    
025    import javax.xml.transform.TransformerException;
026    
027    import org.apache.xpath.XPath;
028    import org.apache.xpath.objects.XObject;
029    import org.apache.xpath.res.XPATHErrorResources;
030    import org.apache.xpath.res.XPATHMessages;
031    import org.w3c.dom.DOMException;
032    import org.w3c.dom.Node;
033    import org.w3c.dom.NodeList;
034    import org.w3c.dom.events.Event;
035    import org.w3c.dom.events.EventListener;
036    import org.w3c.dom.events.EventTarget;
037    import org.w3c.dom.traversal.NodeIterator;
038    import org.w3c.dom.xpath.XPathException;
039    import org.w3c.dom.xpath.XPathResult;
040    
041    /**
042     *
043     * The class provides an implementation XPathResult according 
044     * to the DOM L3 XPath Specification, Working Group Note 26 February 2004.
045     *
046     * <p>See also the <a href='http://www.w3.org/TR/2004/NOTE-DOM-Level-3-XPath-20040226'>Document Object Model (DOM) Level 3 XPath Specification</a>.</p>
047     * 
048     * <p>The <code>XPathResult</code> interface represents the result of the 
049     * evaluation of an XPath expression within the context of a particular 
050     * node. Since evaluation of an XPath expression can result in various 
051     * result types, this object makes it possible to discover and manipulate 
052     * the type and value of the result.</p>
053     * 
054     * <p>This implementation wraps an <code>XObject</code>.
055     * 
056     * @see org.apache.xpath.objects.XObject
057     * @see org.w3c.dom.xpath.XPathResult
058     * 
059     * @xsl.usage internal
060     */
061    class XPathResultImpl implements XPathResult, EventListener {
062    
063            /**
064         *  The wrapped XObject
065         */
066            final private XObject m_resultObj;
067            
068            /**
069             * The xpath object that wraps the expression used for this result.
070             */
071            final private XPath m_xpath;
072                    
073            /**
074         *  This the type specified by the user during construction.  Typically
075             *  the constructor will be called by org.apache.xpath.XPath.evaluate().
076         */
077            final private short m_resultType; 
078            
079            private boolean m_isInvalidIteratorState = false;
080            
081        /**
082         * Only used to attach a mutation event handler when specified
083         * type is an iterator type.
084         */
085            final private Node m_contextNode;
086            
087            /**
088         *  The iterator, if this is an iterator type.
089         */
090            private NodeIterator m_iterator = null;;
091            
092            /**
093         *  The list, if this is a snapshot type.
094         */
095            private NodeList m_list = null;
096            
097    
098            /**
099             * Constructor for XPathResultImpl.
100         * 
101         * For internal use only.
102             */
103             XPathResultImpl(short type, XObject result, Node contextNode, XPath xpath) {
104                    // Check that the type is valid
105                    if (!isValidType(type)) {
106                String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_INVALID_XPATH_TYPE, new Object[] {new Integer(type)});       
107                throw new XPathException(XPathException.TYPE_ERR,fmsg); // Invalid XPath type argument: {0}           
108                    }
109    
110            // Result object should never be null!
111            if (null == result) {
112                String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_EMPTY_XPATH_RESULT, null);       
113                throw new XPathException(XPathException.INVALID_EXPRESSION_ERR,fmsg); // Empty XPath result object
114            }
115            
116            this.m_resultObj = result;
117            this.m_contextNode = contextNode;
118            this.m_xpath = xpath;
119    
120            // If specified result was ANY_TYPE, determine XObject type
121            if (type == ANY_TYPE) {
122                this.m_resultType = getTypeFromXObject(result);
123            } else {
124                this.m_resultType = type;
125            }
126                    
127            // If the context node supports DOM Events and the type is one of the iterator
128            // types register this result as an event listener
129            if (((m_resultType == XPathResult.ORDERED_NODE_ITERATOR_TYPE) ||
130                (m_resultType == XPathResult.UNORDERED_NODE_ITERATOR_TYPE))) {
131                    addEventListener();
132                    
133            }// else can we handle iterator types if contextNode doesn't support EventTarget??
134                                    
135            // If this is an iterator type get the iterator
136            if ((m_resultType == ORDERED_NODE_ITERATOR_TYPE) ||
137                (m_resultType == UNORDERED_NODE_ITERATOR_TYPE) ||
138                (m_resultType == ANY_UNORDERED_NODE_TYPE) ||
139                (m_resultType == FIRST_ORDERED_NODE_TYPE))  {
140        
141                try {
142                    m_iterator = m_resultObj.nodeset();
143                } catch (TransformerException te) {
144                    // probably not a node type
145                                    String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_INCOMPATIBLE_TYPES, new Object[] {m_xpath.getPatternString(), getTypeString(getTypeFromXObject(m_resultObj)),getTypeString(m_resultType)});       
146                                throw new XPathException(XPathException.TYPE_ERR, fmsg);  // "The XPathResult of XPath expression {0} has an XPathResultType of {1} which cannot be coerced into the specified XPathResultType of {2}."},
147               }
148        
149                    // If user requested ordered nodeset and result is unordered 
150                    // need to sort...TODO
151        //            if ((m_resultType == ORDERED_NODE_ITERATOR_TYPE) &&
152        //                (!(((DTMNodeIterator)m_iterator).getDTMIterator().isDocOrdered()))) {
153        // 
154        //            }
155        
156            // If it's a snapshot type, get the nodelist
157            } else if ((m_resultType == UNORDERED_NODE_SNAPSHOT_TYPE) ||
158                       (m_resultType == ORDERED_NODE_SNAPSHOT_TYPE)) {
159                try {               
160                       m_list = m_resultObj.nodelist();
161                } catch (TransformerException te) {
162                            // probably not a node type 
163                                    String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_INCOMPATIBLE_TYPES, new Object[] {m_xpath.getPatternString(), getTypeString(getTypeFromXObject(m_resultObj)),getTypeString(m_resultType)});       
164                                    throw new XPathException(XPathException.TYPE_ERR, fmsg); // "The XPathResult of XPath expression {0} has an XPathResultType of {1} which cannot be coerced into the specified XPathResultType of {2}."},
165                }
166            }                
167            }
168    
169            /**
170             * @see org.w3c.dom.xpath.XPathResult#getResultType()
171             */
172            public short getResultType() {
173                    return m_resultType;
174            }
175    
176            /**
177             *  The value of this number result.
178         * @exception XPathException
179         *   TYPE_ERR: raised if <code>resultType</code> is not 
180         *   <code>NUMBER_TYPE</code>.
181             * @see org.w3c.dom.xpath.XPathResult#getNumberValue()
182             */
183            public double getNumberValue() throws XPathException {
184                    if (getResultType() != NUMBER_TYPE) {
185                            String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_CANT_CONVERT_XPATHRESULTTYPE_TO_NUMBER, new Object[] {m_xpath.getPatternString(), getTypeString(m_resultType)});          
186                            throw new XPathException(XPathException.TYPE_ERR,fmsg);
187    //              "The XPathResult of XPath expression {0} has an XPathResultType of {1} which cannot be converted to a number"
188                    } else {
189                            try {
190                               return m_resultObj.num();
191                            } catch (Exception e) {
192                                    // Type check above should prevent this exception from occurring.
193                                    throw new XPathException(XPathException.TYPE_ERR,e.getMessage());
194                            }
195                    }               
196            }
197    
198            /**
199             * The value of this string result.
200         * @exception XPathException
201         *   TYPE_ERR: raised if <code>resultType</code> is not 
202         *   <code>STRING_TYPE</code>.
203         * 
204             * @see org.w3c.dom.xpath.XPathResult#getStringValue()
205             */
206            public String getStringValue() throws XPathException {
207                    if (getResultType() != STRING_TYPE) {
208                            String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_CANT_CONVERT_TO_STRING, new Object[] {m_xpath.getPatternString(), m_resultObj.getTypeString()});          
209                            throw new XPathException(XPathException.TYPE_ERR,fmsg);
210    //              "The XPathResult of XPath expression {0} has an XPathResultType of {1} which cannot be converted to a string."
211                    } else {
212                            try {
213                               return m_resultObj.str();
214                            } catch (Exception e) {
215                                    // Type check above should prevent this exception from occurring.
216                                    throw new XPathException(XPathException.TYPE_ERR,e.getMessage());
217                            }
218                    }
219            }
220    
221            /**
222             * @see org.w3c.dom.xpath.XPathResult#getBooleanValue()
223             */
224            public boolean getBooleanValue() throws XPathException {
225                    if (getResultType() != BOOLEAN_TYPE) {
226                            String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_CANT_CONVERT_TO_BOOLEAN, new Object[] {m_xpath.getPatternString(), getTypeString(m_resultType)});         
227                            throw new XPathException(XPathException.TYPE_ERR,fmsg);
228    //              "The XPathResult of XPath expression {0} has an XPathResultType of {1} which cannot be converted to a boolean."                 
229                    } else {
230                            try {
231                               return m_resultObj.bool();
232                            } catch (TransformerException e) {
233                                    // Type check above should prevent this exception from occurring.
234                                    throw new XPathException(XPathException.TYPE_ERR,e.getMessage());
235                            }
236                    }
237            }
238    
239            /**
240             * The value of this single node result, which may be <code>null</code>.
241         * @exception XPathException
242         *   TYPE_ERR: raised if <code>resultType</code> is not 
243         *   <code>ANY_UNORDERED_NODE_TYPE</code> or 
244         *   <code>FIRST_ORDERED_NODE_TYPE</code>.
245         * 
246             * @see org.w3c.dom.xpath.XPathResult#getSingleNodeValue()
247             */
248            public Node getSingleNodeValue() throws XPathException {
249                    
250                    if ((m_resultType != ANY_UNORDERED_NODE_TYPE) &&
251                        (m_resultType != FIRST_ORDERED_NODE_TYPE)) {
252                                    String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_CANT_CONVERT_TO_SINGLENODE, new Object[] {m_xpath.getPatternString(), getTypeString(m_resultType)});                        
253                                    throw new XPathException(XPathException.TYPE_ERR,fmsg);
254    //                              "The XPathResult of XPath expression {0} has an XPathResultType of {1} which cannot be converted to a single node. 
255    //                               This method applies only to types ANY_UNORDERED_NODE_TYPE and FIRST_ORDERED_NODE_TYPE."
256                }
257            
258                    NodeIterator result = null;
259                    try {
260                            result = m_resultObj.nodeset();
261                    } catch (TransformerException te) {
262                            throw new XPathException(XPathException.TYPE_ERR,te.getMessage());
263                    }
264            
265            if (null == result) return null;
266            
267            Node node = result.nextNode();
268             
269            // Wrap "namespace node" in an XPathNamespace 
270            if (isNamespaceNode(node)) {
271                return new XPathNamespaceImpl(node);
272            } else {
273                return node;
274            }        
275            }
276    
277            /**
278             * @see org.w3c.dom.xpath.XPathResult#getInvalidIteratorState()
279             */
280            public boolean getInvalidIteratorState() {
281                    return m_isInvalidIteratorState;
282            }
283    
284            /**
285             * The number of nodes in the result snapshot. Valid values for 
286         * snapshotItem indices are <code>0</code> to 
287         * <code>snapshotLength-1</code> inclusive.
288         * @exception XPathException
289         *   TYPE_ERR: raised if <code>resultType</code> is not 
290         *   <code>UNORDERED_NODE_SNAPSHOT_TYPE</code> or 
291         *   <code>ORDERED_NODE_SNAPSHOT_TYPE</code>.
292         * 
293             * @see org.w3c.dom.xpath.XPathResult#getSnapshotLength()
294             */
295            public int getSnapshotLength() throws XPathException {
296            
297                    if ((m_resultType != UNORDERED_NODE_SNAPSHOT_TYPE) &&
298                        (m_resultType != ORDERED_NODE_SNAPSHOT_TYPE)) {
299                                    String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_CANT_GET_SNAPSHOT_LENGTH, new Object[] {m_xpath.getPatternString(), getTypeString(m_resultType)});                        
300                                    throw new XPathException(XPathException.TYPE_ERR,fmsg); 
301    //                              "The method getSnapshotLength cannot be called on the XPathResult of XPath expression {0} because its XPathResultType is {1}.
302                }
303                            
304                    return m_list.getLength();
305            }
306    
307            /**
308             * Iterates and returns the next node from the node set or 
309         * <code>null</code>if there are no more nodes.
310         * @return Returns the next node.
311         * @exception XPathException
312         *   TYPE_ERR: raised if <code>resultType</code> is not 
313         *   <code>UNORDERED_NODE_ITERATOR_TYPE</code> or 
314         *   <code>ORDERED_NODE_ITERATOR_TYPE</code>.
315         * @exception DOMException
316         *   INVALID_STATE_ERR: The document has been mutated since the result was 
317         *   returned.
318             * @see org.w3c.dom.xpath.XPathResult#iterateNext()
319             */
320            public Node iterateNext() throws XPathException, DOMException {
321                    if ((m_resultType != UNORDERED_NODE_ITERATOR_TYPE) &&
322                        (m_resultType != ORDERED_NODE_ITERATOR_TYPE)) {
323              String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_NON_ITERATOR_TYPE, new Object[] {m_xpath.getPatternString(), getTypeString(m_resultType)});                        
324                      throw new XPathException(XPathException.TYPE_ERR, fmsg);  
325    //                "The method iterateNext cannot be called on the XPathResult of XPath expression {0} because its XPathResultType is {1}. 
326    //                This method applies only to types UNORDERED_NODE_ITERATOR_TYPE and ORDERED_NODE_ITERATOR_TYPE."},
327                }
328    
329                    if (getInvalidIteratorState()) {
330              String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_DOC_MUTATED, null);                        
331                      throw new DOMException(DOMException.INVALID_STATE_ERR,fmsg);  // Document mutated since result was returned. Iterator is invalid.
332                    }                        
333    
334            Node node = m_iterator.nextNode();
335            if(null == node)
336                    removeEventListener(); // JIRA 1673
337            // Wrap "namespace node" in an XPathNamespace 
338            if (isNamespaceNode(node)) {
339                return new XPathNamespaceImpl(node);
340            } else {
341                return node;
342            }
343            }
344    
345        /**
346         * Returns the <code>index</code>th item in the snapshot collection. If 
347         * <code>index</code> is greater than or equal to the number of nodes in 
348         * the list, this method returns <code>null</code>. Unlike the iterator 
349         * result, the snapshot does not become invalid, but may not correspond 
350         * to the current document if it is mutated.
351         * @param index Index into the snapshot collection.
352         * @return The node at the <code>index</code>th position in the 
353         *   <code>NodeList</code>, or <code>null</code> if that is not a valid 
354         *   index.
355         * @exception XPathException
356         *   TYPE_ERR: raised if <code>resultType</code> is not 
357         *   <code>UNORDERED_NODE_SNAPSHOT_TYPE</code> or 
358         *   <code>ORDERED_NODE_SNAPSHOT_TYPE</code>.
359         *
360             * @see org.w3c.dom.xpath.XPathResult#snapshotItem(int)
361             */
362            public Node snapshotItem(int index) throws XPathException {
363                    
364                    if ((m_resultType != UNORDERED_NODE_SNAPSHOT_TYPE) &&
365                        (m_resultType != ORDERED_NODE_SNAPSHOT_TYPE)) {
366               String fmsg = XPATHMessages.createXPATHMessage(XPATHErrorResources.ER_NON_SNAPSHOT_TYPE, new Object[] {m_xpath.getPatternString(), getTypeString(m_resultType)});                        
367               throw new XPathException(XPathException.TYPE_ERR, fmsg); 
368    //              "The method snapshotItem cannot be called on the XPathResult of XPath expression {0} because its XPathResultType is {1}. 
369    //              This method applies only to types UNORDERED_NODE_SNAPSHOT_TYPE and ORDERED_NODE_SNAPSHOT_TYPE."},
370                }           
371            
372            Node node = m_list.item(index);
373            
374            // Wrap "namespace node" in an XPathNamespace 
375            if (isNamespaceNode(node)) {
376                return new XPathNamespaceImpl(node);
377            } else {
378                return node;
379            }
380            }
381    
382            
383            /**
384             * Check if the specified type is one of the supported types.
385             * @param type The specified type
386             * 
387             * @return true If the specified type is supported; otherwise, returns false.
388             */
389            static boolean isValidType( short type ) {
390                    switch (type) {
391                            case ANY_TYPE:
392                            case NUMBER_TYPE:
393                            case STRING_TYPE:
394                            case BOOLEAN_TYPE:
395                            case UNORDERED_NODE_ITERATOR_TYPE:
396                            case ORDERED_NODE_ITERATOR_TYPE:
397                            case UNORDERED_NODE_SNAPSHOT_TYPE:
398                            case ORDERED_NODE_SNAPSHOT_TYPE:
399                            case ANY_UNORDERED_NODE_TYPE:
400                            case FIRST_ORDERED_NODE_TYPE: return true;
401                            default: return false;
402                    }
403            }
404    
405            /**
406             * @see org.w3c.dom.events.EventListener#handleEvent(Event)
407             */
408            public void handleEvent(Event event) {
409                    
410                    if (event.getType().equals("DOMSubtreeModified")) {
411                            // invalidate the iterator
412                            m_isInvalidIteratorState = true;
413                            
414                            // deregister as a listener to reduce computational load
415                            removeEventListener();
416                    }
417            }
418        
419      /**
420       * Given a request type, return the equivalent string.
421       * For diagnostic purposes.
422       *
423       * @return type string 
424       */
425      private String getTypeString(int type)
426      {
427         switch (type) {
428          case ANY_TYPE: return "ANY_TYPE";
429          case ANY_UNORDERED_NODE_TYPE: return "ANY_UNORDERED_NODE_TYPE";
430          case BOOLEAN_TYPE: return "BOOLEAN";
431          case FIRST_ORDERED_NODE_TYPE: return "FIRST_ORDERED_NODE_TYPE";
432          case NUMBER_TYPE: return "NUMBER_TYPE";
433          case ORDERED_NODE_ITERATOR_TYPE: return "ORDERED_NODE_ITERATOR_TYPE";
434          case ORDERED_NODE_SNAPSHOT_TYPE: return "ORDERED_NODE_SNAPSHOT_TYPE";
435          case STRING_TYPE: return "STRING_TYPE";
436          case UNORDERED_NODE_ITERATOR_TYPE: return "UNORDERED_NODE_ITERATOR_TYPE";
437          case UNORDERED_NODE_SNAPSHOT_TYPE: return "UNORDERED_NODE_SNAPSHOT_TYPE";
438          default: return "#UNKNOWN";
439        }
440      }  
441      
442      /**
443       * Given an XObject, determine the corresponding DOM XPath type
444       * 
445       * @return type string
446       */
447      private short getTypeFromXObject(XObject object) {
448          switch (object.getType()) {
449            case XObject.CLASS_BOOLEAN: return BOOLEAN_TYPE;
450            case XObject.CLASS_NODESET: return UNORDERED_NODE_ITERATOR_TYPE; 
451            case XObject.CLASS_NUMBER: return NUMBER_TYPE;
452            case XObject.CLASS_STRING: return STRING_TYPE;
453            // XPath 2.0 types                         
454    //          case XObject.CLASS_DATE: 
455    //          case XObject.CLASS_DATETIME:
456    //          case XObject.CLASS_DTDURATION:
457    //          case XObject.CLASS_GDAY:
458    //          case XObject.CLASS_GMONTH:
459    //          case XObject.CLASS_GMONTHDAY:
460    //          case XObject.CLASS_GYEAR:
461    //          case XObject.CLASS_GYEARMONTH: 
462    //          case XObject.CLASS_TIME:
463    //          case XObject.CLASS_YMDURATION: return STRING_TYPE; // treat all date types as strings?
464          
465            case XObject.CLASS_RTREEFRAG: return UNORDERED_NODE_ITERATOR_TYPE; 
466            case XObject.CLASS_NULL: return ANY_TYPE; // throw exception ?
467            default: return ANY_TYPE; // throw exception ?
468        }     
469        
470      }  
471    
472    /**
473     * Given a node, determine if it is a namespace node.
474     * 
475     * @param node 
476     * 
477     * @return boolean Returns true if this is a namespace node; otherwise, returns false.
478     */
479      private boolean isNamespaceNode(Node node) {
480        
481         if ((null != node) && 
482             (node.getNodeType() == Node.ATTRIBUTE_NODE) &&
483             (node.getNodeName().startsWith("xmlns:") || node.getNodeName().equals("xmlns"))) {
484            return true;   
485         } else {
486            return false;
487         }
488      }
489      
490    /**
491     * Add m_contextNode to Event Listner to listen for Mutations Events
492     *
493     */
494      private void addEventListener(){
495            if(m_contextNode instanceof EventTarget)
496                    ((EventTarget)m_contextNode).addEventListener("DOMSubtreeModified",this,true);
497            
498      }
499      
500    
501    /**
502     * Remove m_contextNode to Event Listner to listen for Mutations Events
503     *
504     */
505    private void removeEventListener(){
506            if(m_contextNode instanceof EventTarget)
507                    ((EventTarget)m_contextNode).removeEventListener("DOMSubtreeModified",this,true);
508    }
509      
510    }