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 */
017package org.apache.commons.jxpath.ri;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.HashSet;
022import java.util.Iterator;
023import java.util.List;
024import java.util.NoSuchElementException;
025
026import org.apache.commons.jxpath.BasicNodeSet;
027import org.apache.commons.jxpath.ExpressionContext;
028import org.apache.commons.jxpath.JXPathContext;
029import org.apache.commons.jxpath.JXPathException;
030import org.apache.commons.jxpath.NodeSet;
031import org.apache.commons.jxpath.Pointer;
032import org.apache.commons.jxpath.ri.axes.RootContext;
033import org.apache.commons.jxpath.ri.model.NodePointer;
034import org.apache.commons.jxpath.util.ReverseComparator;
035
036/**
037 * An XPath evaluation context.
038 *
039 * When  evaluating a path, a chain of EvalContexts is created, each context in
040 * the chain representing a step of the path. Subclasses of EvalContext
041 * implement behavior of various XPath axes: "child::", "parent::" etc.
042 *
043 * @author Dmitri Plotnikov
044 * @version $Revision: 652845 $ $Date: 2008-05-02 12:46:46 -0500 (Fri, 02 May 2008) $
045 */
046public abstract class EvalContext implements ExpressionContext, Iterator {
047    /** parent context */
048    protected EvalContext parentContext;
049
050    /** root context */
051    protected RootContext rootContext;
052
053    /** position */
054    protected int position = 0;
055
056    private boolean startedSetIteration = false;
057    private boolean done = false;
058    private boolean hasPerformedIteratorStep = false;
059    private Iterator pointerIterator;
060
061    /**
062     * Create a new EvalContext.
063     * @param parentContext parent context
064     */
065    public EvalContext(EvalContext parentContext) {
066        this.parentContext = parentContext;
067    }
068
069    public Pointer getContextNodePointer() {
070        return getCurrentNodePointer();
071    }
072
073    public JXPathContext getJXPathContext() {
074        return getRootContext().getJXPathContext();
075    }
076
077    public int getPosition() {
078        return position;
079    }
080
081    /**
082     * Determines the document order for this context.
083     *
084     * @return 1 ascending order, -1 descending order,
085     *  0 - does not require ordering
086     */
087    public int getDocumentOrder() {
088        return parentContext != null && parentContext.isChildOrderingRequired() ? 1 : 0;
089    }
090
091    /**
092     * Even if this context has the natural ordering and therefore does
093     * not require collecting and sorting all nodes prior to returning them,
094     * such operation may be required for any child context.
095     * @return boolean
096     */
097    public boolean isChildOrderingRequired() {
098        // Default behavior: if this context needs to be ordered,
099        // the children need to be ordered too
100        return getDocumentOrder() != 0;
101    }
102
103    /**
104     * Returns true if there are mode nodes matching the context's constraints.
105     * @return boolean
106     */
107    public boolean hasNext() {
108        if (pointerIterator != null) {
109            return pointerIterator.hasNext();
110        }
111        if (getDocumentOrder() != 0) {
112            return constructIterator();
113        }
114        if (!done && !hasPerformedIteratorStep) {
115            performIteratorStep();
116        }
117        return !done;
118    }
119
120    /**
121     * Returns the next node pointer in the context
122     * @return Object
123     */
124    public Object next() {
125        if (pointerIterator != null) {
126            return pointerIterator.next();
127        }
128
129        if (getDocumentOrder() != 0) {
130            if (!constructIterator()) {
131                throw new NoSuchElementException();
132            }
133            return pointerIterator.next();
134        }
135        if (!done && !hasPerformedIteratorStep) {
136            performIteratorStep();
137        }
138        if (done) {
139            throw new NoSuchElementException();
140        }
141        hasPerformedIteratorStep = false;
142        return getCurrentNodePointer();
143    }
144
145    /**
146     * Moves the iterator forward by one position
147     */
148    private void performIteratorStep() {
149        done = true;
150        if (position != 0 && nextNode()) {
151            done = false;
152        }
153        else {
154            while (nextSet()) {
155                if (nextNode()) {
156                    done = false;
157                    break;
158                }
159            }
160        }
161        hasPerformedIteratorStep = true;
162    }
163
164    /**
165     * Operation is not supported
166     * @throws UnsupportedOperationException
167     */
168    public void remove() {
169        throw new UnsupportedOperationException(
170            "JXPath iterators cannot remove nodes");
171    }
172
173    /**
174     * Construct an iterator.
175     * @return whether the Iterator was constructed
176     */
177    private boolean constructIterator() {
178        HashSet set = new HashSet();
179        ArrayList list = new ArrayList();
180        while (nextSet()) {
181            while (nextNode()) {
182                NodePointer pointer = getCurrentNodePointer();
183                if (!set.contains(pointer)) {
184                    set.add(pointer);
185                    list.add(pointer);
186                }
187            }
188        }
189        if (list.isEmpty()) {
190            return false;
191        }
192
193        sortPointers(list);
194
195        pointerIterator = list.iterator();
196        return true;
197    }
198
199    /**
200     * Sort a list of pointers based on document order.
201     * @param l the list to sort.
202     */
203    protected void sortPointers(List l) {
204        switch (getDocumentOrder()) {
205        case 1:
206            Collections.sort(l);
207            break;
208        case -1:
209            Collections.sort(l, ReverseComparator.INSTANCE);
210            break;
211        default:
212        }
213    }
214
215    /**
216     * Returns the list of all Pointers in this context for the current
217     * position of the parent context.
218     * @return List
219     */
220    public List getContextNodeList() {
221        int pos = position;
222        if (pos != 0) {
223            reset();
224        }
225        List list = new ArrayList();
226        while (nextNode()) {
227            list.add(getCurrentNodePointer());
228        }
229        if (pos != 0) {
230            setPosition(pos);
231        }
232        else {
233            reset();
234        }
235        return list;
236    }
237
238    /**
239     * Returns the list of all Pointers in this context for all positions
240     * of the parent contexts.  If there was an ongoing iteration over
241     * this context, the method should not be called.
242     * @return NodeSet
243     */
244    public NodeSet getNodeSet() {
245        if (position != 0) {
246            throw new JXPathException(
247                "Simultaneous operations: "
248                    + "should not request pointer list while "
249                    + "iterating over an EvalContext");
250        }
251        BasicNodeSet set = new BasicNodeSet();
252        while (nextSet()) {
253            while (nextNode()) {
254                set.add((Pointer) getCurrentNodePointer().clone());
255            }
256        }
257
258        return set;
259    }
260
261    /**
262     * Typically returns the NodeSet by calling getNodeSet(),
263     * but will be overridden for contexts that more naturally produce
264     * individual values, e.g. VariableContext
265     * @return Object
266     */
267    public Object getValue() {
268        return getNodeSet();
269    }
270
271    public String toString() {
272        Pointer ptr = getContextNodePointer();
273        return ptr == null ? "Empty expression context" : "Expression context [" + getPosition()
274                + "] " + ptr.asPath();
275    }
276
277    /**
278     * Returns the root context of the path, which provides easy
279     * access to variables and functions.
280     * @return RootContext
281     */
282    public RootContext getRootContext() {
283        if (rootContext == null) {
284            rootContext = parentContext.getRootContext();
285        }
286        return rootContext;
287    }
288
289    /**
290     * Sets current position = 0, which is the pre-iteration state.
291     */
292    public void reset() {
293        position = 0;
294    }
295
296    /**
297     * Get the current position.
298     * @return int position.
299     */
300    public int getCurrentPosition() {
301        return position;
302    }
303
304    /**
305     * Returns the first encountered Pointer that matches the current
306     * context's criteria.
307     * @return Pointer
308     */
309    public Pointer getSingleNodePointer() {
310        reset();
311        while (nextSet()) {
312            if (nextNode()) {
313                return getCurrentNodePointer();
314            }
315        }
316        return null;
317    }
318
319    /**
320     * Returns the current context node. Undefined before the beginning
321     * of the iteration.
322     * @return NodePoiner
323     */
324    public abstract NodePointer getCurrentNodePointer();
325
326    /**
327     * Returns true if there is another sets of objects to interate over.
328     * Resets the current position and node.
329     * @return boolean
330     */
331    public boolean nextSet() {
332        reset(); // Restart iteration within the set
333
334        // Most of the time you have one set per parent node
335        // First time this method is called, we should look for
336        // the first parent set that contains at least one node.
337        if (!startedSetIteration) {
338            startedSetIteration = true;
339            while (parentContext.nextSet()) {
340                if (parentContext.nextNode()) {
341                    return true;
342                }
343            }
344            return false;
345        }
346
347        // In subsequent calls, we see if the parent context
348        // has any nodes left in the current set
349        if (parentContext.nextNode()) {
350            return true;
351        }
352
353        // If not, we look for the next set that contains
354        // at least one node
355        while (parentContext.nextSet()) {
356            if (parentContext.nextNode()) {
357                return true;
358            }
359        }
360        return false;
361    }
362
363    /**
364     * Returns true if there is another object in the current set.
365     * Switches the current position and node to the next object.
366     * @return boolean
367     */
368    public abstract boolean nextNode();
369
370    /**
371     * Moves the current position to the specified index. Used with integer
372     * predicates to quickly get to the n'th element of the node set.
373     * Returns false if the position is out of the node set range.
374     * You can call it with 0 as the position argument to restart the iteration.
375     * @param position to set
376     * @return boolean
377     */
378    public boolean setPosition(int position) {
379        this.position = position;
380        return true;
381    }
382}