blob: 196571f4d3b2c1ee6ca1e6c589a4ceeb2e7c630b [file] [log] [blame]
package org.apache.velocity.tools.generic;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;
import org.apache.velocity.tools.ClassUtils;
import org.apache.velocity.tools.Scope;
import org.apache.velocity.tools.config.DefaultKey;
import org.apache.velocity.tools.config.ValidScope;
/**
* <p>
* A convenience tool to use with #foreach loops. It wraps a list
* with a custom iterator to provide additional controls and feedback
* for managing loops.
* </p>
* <p>
* This tool was originally inspired the now-deprecated IteratorTool,
* which provided similar base functionality but was somewhat more difficult
* to understand and use. Rather than try to migrate that implementation
* via deprecation and new methods, it was simplest to just create an
* entirely new tool that simplified the original API and was easy
* to augment with useful new features like support for nested
* (and nameable) loops, skipping ahead in loops, synchronizing multiple
* iterators, getting the iteration count of loops, identifying if a loop is
* on its first or last iteration, and so on.
* </p>
* <p>
* Most functions of this tool will be obsolete with the release of
* Velocity 1.7, which will provide $foreach.hasNext, $foreach.isFirst,
* $foreach.isLast, $foreach.index and $foreach.count automatically.
* However, this will still be useful for the more advanced sync
* and skip features. Also, for very complicated nested loops, the
* loop naming feature may be easier than doing things like $foreach.parent.parent.
* </p>
* <p>
* Example of use:
* </p>
* <pre>
* Template
* ---
* #set( $list = [1..7] )
* #set( $others = [3..10] )
* #foreach( $item in $loop.watch($list).sync($others, 'other') )
* $item -&gt; $loop.other
* #if( $item &gt;= 5 )$loop.stop()#end
* #end
*
* Output
* ------
* 1 -&gt; 3
* 2 -&gt; 4
* 3 -&gt; 5
* 4 -&gt; 6
* 5 -&gt; 7
*
* Example tools.xml config (if you want to use this with VelocityView):
* &lt;tools&gt;
* &lt;toolbox scope="request"&gt;
* &lt;tool class="org.apache.velocity.tools.generic.LoopTool"/&gt;
* &lt;/toolbox&gt;
* &lt;/tools&gt;
* </pre>
*
* @author Nathan Bubna
* @version $Id: LoopTool.java 590893 2007-11-01 04:40:21Z nbubna $
*/
@DefaultKey("loop")
@ValidScope(Scope.REQUEST)
public class LoopTool extends SafeConfig implements Serializable
{
private static final long serialVersionUID = -4760226781390687478L;
private Stack<ManagedIterator> iterators = new Stack<ManagedIterator>();
private ManagedIterator last;
private Map<String,Object> lastSyncedValues;
/**
* <p>Tells the LoopTool to watch the specified Array, Collection, Map,
* Iterator, Iterable, Enumeration or POJO with an iterator() method
* while the template iterates over the values within it.
* </p>
* <p>Under the covers, this is returning an iterable wrapper that
* is also pushed onto this tool's stack. That allows this tool to
* know which iterator to give later commands (i.e. stop() or skip()).
* </p>
* @param obj an object that Velocity's #foreach directive can iterate over
* @return a {@link ManagedIterator} that this tool instance will track
*/
public ManagedIterator watch(Object obj)
{
Iterator iterator = getIterator(obj);
if (iterator == null)
{
return null;
}
ManagedIterator managed = manage(iterator, null);
iterators.push(managed);
this.last = managed;
return managed;
}
/**
* This is just like {@link #watch(Object)} except that it also takes
* a name which is given to the {@link ManagedIterator} that is returned.
* This allows the user to send stop or skip commands to that specific
* iterator even when there are nested iterators within it that are being
* watched. If the given name is {@code null}, then this will return
* {@code null} even if the object can be watched. Provided names cannot
* be {@code null}.
* @param obj an object that Velocity's #foreach directive can iterate over
* @param name loop name
* @return a {@link ManagedIterator} that this tool instance will track
* @see #watch(Object)
*/
public ManagedIterator watch(Object obj, String name)
{
// don't mess around with null names
if (name == null)
{
return null;
}
Iterator iterator = getIterator(obj);
if (iterator == null)
{
return null;
}
ManagedIterator managed = manage(iterator, name);
iterators.push(managed);
this.last = managed;
return managed;
}
public ManagedIterator sync(Object main, Object synced)
{
return watch(main).sync(synced);
}
protected ManagedIterator manage(Iterator iterator, String name)
{
return new ManagedIterator(name, iterator, this);
}
/**
* This tells the current loop to stop after the current iteration.
* This is different from "break" common to most programming languages,
* in that it does not immediately cease activity in the current iteration.
* Instead, it merely tells the #foreach loop that this is the last time
* around.
*/
public void stop()
{
// if we have an iterator on the stack
if (!iterators.empty())
{
// stop the top one, so #foreach doesn't loop again
iterators.peek().stop();
}
}
/**
* This is just like {@link #stop()} except that the stop command is issued
* <strong>only</strong> to the loop/iterator with the specified name.
* If no such loop is found with that name, then no stop command is issued.
* @param name loop name
* @see #stop()
*/
public void stop(String name)
{
// just stop the matching one
for (ManagedIterator iterator : iterators)
{
if (iterator.getName().equals(name))
{
iterator.stop();
break;
}
}
}
/**
* This is just like {@link #stop(String)} except that the stop command is issued
* both to the loop/iterator with the specified name and all loops nested within
* it. If no such loop is found with that name, then no stop commands are
* issued.
* @param name loop name
* @see #stop()
* @see #stop(String)
*/
public void stopTo(String name)
{
if (!iterators.empty())
{
// create a backup stack to put things back as they were
Stack<ManagedIterator> backup = new Stack<ManagedIterator>();
// look for the iterator with the specified name
boolean found = false;
while (!found && !iterators.empty())
{
ManagedIterator iterator = iterators.pop();
if (iterator.getName().equals(name))
{
found = true;
iterator.stop();
}
else
{
// keep a backup of the ones that don't match
backup.push(iterator);
}
}
while (!backup.empty())
{
// push the nested iterators back
ManagedIterator iterator = backup.pop();
iterators.push(iterator);
if (found)
{
iterator.stop();
}
}
}
}
/**
* This is just like {@link #stop()} except that the stop command is issued
* <strong>all</strong> the loops being watched by this tool.
* @see #stop()
*/
public void stopAll()
{
// just stop them all
for (ManagedIterator iterator : iterators)
{
iterator.stop();
}
}
/**
* Skips ahead the specified number of iterations (if possible).
* Since this is manual skipping (unlike the automatic skipping
* provided by the likes of {@link ManagedIterator#exclude(Object)}, any elements
* skipped are still considered in the results returned by {@link #getCount()}
* and {@link #isFirst()}.
* @param number number of iterations
*/
public void skip(int number)
{
// if we have an iterator on the stack
if (!iterators.empty())
{
// tell the top one to skip the specified number
skip(number, iterators.peek());
}
}
/**
* This tells the specified loop to skip ahead the specified number of
* iterations.
* @param number number of iterations
* @param name loop name
* @see #skip(int)
*/
public void skip(int number, String name)
{
// just tell the matching one to skip
ManagedIterator iterator = findIterator(name);
if (iterator != null)
{
skip(number, iterator);
}
}
// does the actual skipping by manually advancing the ManagedIterator
private void skip(int number, ManagedIterator iterator)
{
for (int i=0; i < number; i++)
{
if (iterator.hasNext())
{
iterator.next();
}
else
{
break;
}
}
}
/**
* @return {@code true} if the current loop is on its first iteration.
*/
public Boolean isFirst()
{
if (last != null)
{
return last.isFirst();
}
return null;
}
/**
* @return {@code true} if the loop with the specified name
* is on its first iteration.
* @param name loop name
*/
public Boolean isFirst(String name)
{
// just tell the matching one to skip
ManagedIterator iterator = findIterator(name);
if (iterator != null)
{
return iterator.isFirst();
}
return null;
}
/**
* @return the result of {@link #isFirst}. Exists to allow $loop.first syntax.
*/
public Boolean getFirst()
{
return isFirst();
}
/**
* @return {@code true} if the current loop is on its last iteration.
*/
public Boolean isLast()
{
if (last != null)
{
return last.isLast();
}
return null;
}
/**
* @return {@code true} if the loop with the specified name
* is on its last iteration.
* @param name loop name
*/
public Boolean isLast(String name)
{
// just tell the matching one to skip
ManagedIterator iterator = findIterator(name);
if (iterator != null)
{
return iterator.isLast();
}
return null;
}
/**
* @return the result of {@link #isLast}. Exists to allow $loop.last syntax.
*/
public Boolean getLast()
{
return isLast();
}
/**
* <p>This serves two purposes:</p>
* <ul><li>Getting the current value of a sync'ed iterator</li>
* <li>Abbreviate syntax for properties of outer loops</li></ul>
* <p>First, it searches all the loops being managed for one
* with a sync'ed Iterator under the specified name and
* returns the current value for that sync'ed iterator,
* if any. If there is no sync'ed iterators or none with
* that name, then this will check if the specified key
* is requesting a "property" of an outer loop (e.g.
* {@code $loop.count_foo} or {@code $loop.first_foo}).
* This syntax is shorter and clearer than {@code $loop.getCount('foo')}.
* If the key starts with a property name and ends with an outer loop
* name, then the value of that property for that loop is returned.
* @param key sync'ed Iterator name
* @return current iterator value
*/
public Object get(String key)
{
// search all iterators in reverse
// (so nested ones take priority)
// for one that is responsible for synced
for (int i=iterators.size() - 1; i >= 0; i--)
{
ManagedIterator iterator = iterators.get(i);
if (iterator.isSyncedWith(key))
{
return iterator.get(key);
}
}
if (lastSyncedValues != null)
{
Object syncedValue = lastSyncedValues.get(key);
if (syncedValue != null)
{
return syncedValue;
}
}
// shortest key would be "last_X" where X is the loop name
if (key == null || key.length() < 6)
{
return null;
}
if (key.startsWith("last_"))
{
return isLast(key.substring(5, key.length()));
}
if (key.startsWith("count_"))
{
return getCount(key.substring(6, key.length()));
}
if (key.startsWith("index_"))
{
return getIndex(key.substring(6, key.length()));
}
if (key.startsWith("first_"))
{
return isFirst(key.substring(6, key.length()));
}
return null;
}
/**
* Asks the loop with the specified name for the current value
* of the specified sync'ed iterator, if any.
* @param name loop name
* @param synced sync'ed Iterator name
* @return current iterator value
*/
public Object get(String name, String synced)
{
// just ask the matching iterator for the sync'ed value
ManagedIterator iterator = findIterator(name);
if (iterator != null)
{
return iterator.get(synced);
}
return null;
}
/**
* Returns the 0-based index of the item the current loop is handling.
* So, if this is the first iteration, then the index will be 0. If
* you {@link #skip} ahead in this loop, those skipped iterations will
* still be reflected in the index. If iteration has not begun, this
* will return {@code null}.
* @return current loop index
*/
public Integer getIndex()
{
Integer count = getCount();
if (count == null || count == 0)
{
return null;
}
return count - 1;
}
/**
* Returns the 0-based index of the item the specified loop is handling.
* So, if this is the first iteration, then the index will be 0. If
* you {@link #skip} ahead in this loop, those skipped iterations will
* still be reflected in the index. If iteration has not begun, this
* will return {@code null}.
* @param name loop name
* @return current loop index
*/
public Integer getIndex(String name)
{
Integer count = getCount(name);
if (count == null || count == 0)
{
return null;
}
return count - 1;
}
/**
* Returns the number of items the current loop has handled. So, if this
* is the first iteration, then the count will be 1. If you {@link #skip}
* ahead in this loop, those skipped iterations will still be included in
* the count.
* @return items count
*/
public Integer getCount()
{
if (last != null)
{
return last.getCount();
}
return null;
}
/**
* Returns the number of items the specified loop has handled. So, if this
* is the first iteration, then the count will be 1. If you {@link #skip}
* ahead in this loop, those skipped iterations will still be included in
* the count.
* @param name loop name
* @return loop items count
*/
public Integer getCount(String name)
{
// just tell the matching one to skip
ManagedIterator iterator = findIterator(name);
if (iterator != null)
{
return iterator.getCount();
}
return null;
}
/**
* Returns the most recent {@link ManagedIterator} for this instance.
* This can be used to access properties like the count, index,
* isFirst, isLast, etc which would otherwise fail on the last item
* in a loop due to the necessity of popping iterators off the
* stack when the last item is retrieved. (See VELTOOLS-124)
* @return most recent iterator
*/
public ManagedIterator getThis()
{
return last;
}
/**
* Returns the number of loops currently on the stack.
* This is only useful for debugging, as iterators are
* popped off the stack at the start of their final iteration,
* making this frequently "incorrect".
* @return depth
*/
public int getDepth()
{
return iterators.size();
}
/**
* Finds the {@link ManagedIterator} with the specified name
* if it is in this instance's iterator stack.
* @param name loop name
* @return loop iterator
*/
protected ManagedIterator findIterator(String name)
{
// look for the one with the specified name
for (ManagedIterator iterator : iterators)
{
if (iterator.getName().equals(name))
{
return iterator;
}
}
return null;
}
/**
* Don't let templates call this, but allow subclasses
* and ManagedIterator to have access.
* @return pop'ed iterator
*/
protected ManagedIterator pop()
{
ManagedIterator i = iterators.pop();
this.lastSyncedValues = i.getLastSyncedValues();
return i;
}
/**
* Wraps access to {@link ClassUtils#getIterator} is a
* nice little try/catch block to prevent exceptions from
* escaping into the template. In the case of such problems,
* this will return {@code null}.
* @param obj target iterator
* @return wrapped iterator or null
*/
protected Iterator getIterator(Object obj)
{
if (obj == null)
{
return null;
}
try
{
return ClassUtils.getIterator(obj);
}
catch (Exception e)
{
getLog().error("Exception while getting Iterator:", e);
}
return null;
}
/**
* Iterator implementation that wraps a standard {@link Iterator}
* and allows it to be prematurely stopped, skipped ahead, and
* associated with a name for advanced nested loop control.
* This also allows a arbitrary {@link ActionCondition}s to be added
* in order to have it automatically skip over or stop before
* certain elements in the iterator.
*/
public class ManagedIterator implements Iterator
{
private String name;
private Iterator iterator;
private LoopTool owner;
private boolean stopped = false;
private Boolean first = null;
private int count = 0;
private Object next;
private List<ActionCondition> conditions;
private Map<String,SyncedIterator> synced;
public ManagedIterator(String name, Iterator iterator, LoopTool owner)
{
if (name == null)
{
this.name = "loop"+owner.getDepth();
}
else
{
this.name = name;
}
this.iterator = iterator;
this.owner = owner;
}
/**
* @return the name of this instance.
*/
public String getName()
{
return this.name;
}
/**
* @return true if either 0 or 1 elements have been returned
* by {@link #next()}.
*/
public boolean isFirst()
{
if (first == null || first.booleanValue())
{
return true;
}
return false;
}
/**
* @return true if the last element returned by {@link #next()}
* is the last element available in the iterator being managed
* which satisfies any/all {@link ActionCondition}s set for this
* instance. Otherwise, returns false.
*/
public boolean isLast()
{
return !hasNext(false);
}
/**
* @return the result of {@link #isFirst}. Exists to allow $loop.this.first syntax.
*/
public boolean getFirst()
{
return isFirst();
}
/**
* @return the result of {@link #isLast}. Exists to allow $loop.this.last syntax.
*/
public boolean getLast()
{
return isLast();
}
/**
* @return true if there are more elements in the iterator
* being managed by this instance which satisfy all the
* {@link ActionCondition}s set for this instance. Returns
* false if there are no more valid elements available.
*/
public boolean hasNext()
{
return hasNext(true);
}
/**
* @return the result of {@link #hasNext}. Exists to allow $loop.this.hasNext syntax.
*/
public boolean getHasNext()
{
return hasNext(false);//no need to pop, #foreach will always call hasNext()
}
// version that lets isLast check w/o popping this from the stack
private boolean hasNext(boolean popWhenDone)
{
// we don't if we've stopped
if (stopped)
{
return false;
}
// we're not stopped, so do we have a next cached?
if (this.next != null)
{
return true;
}
// try to get a next that satisfies the conditions
// if there isn't one, return false; if there is, return true
return cacheNext(popWhenDone);
}
// Tries to get a next that satisfies the conditions.
// Returns true if there is a next to get.
private boolean cacheNext(boolean popWhenDone)
{
// ok, let's see if we can get a next
if (!iterator.hasNext())
{
if (popWhenDone)
{
// this iterator is done, pop it from the owner's stack
owner.pop();
// and make sure we don't pop twice
stop();
}
return false;
}
// ok, the iterator has more, but do they work for us?
this.next = iterator.next();
if (conditions != null)
{
for (ActionCondition condition : conditions)
{
if (condition.matches(this.next))
{
switch (condition.action)
{
case EXCLUDE:
// recurse on to the next one
return cacheNext(popWhenDone);
case STOP:
stop();
return false;
default:
throw new IllegalStateException("ActionConditions should never have a null Action");
}
}
}
}
return true;
}
private void shiftSynced()
{
if (synced != null)
{
for (SyncedIterator parallel : synced.values())
{
parallel.shift();
}
}
}
/**
* @param name syn'ed iterator name
* @return {@code true} if this ManagedIterator has a sync'ed
* iterator with the specified name.
*/
public boolean isSyncedWith(String name)
{
if (synced == null)
{
return false;
}
return synced.containsKey(name);
}
/**
* @return the parallel value from the specified sync'ed iterator.
* If no sync'ed iterator exists with that name or that iterator
* is finished, this will return {@code null}.
* @param name iterator name
* @return iterator current value
*/
public Object get(String name)
{
if (synced == null)
{
return null;
}
SyncedIterator parallel = synced.get(name);
if (parallel == null)
{
return null;
}
return parallel.get();
}
/**
* @return the number of elements returned by {@link #next()} so far.
*/
public int getCount()
{
return count;
}
/**
* @return the 0-based index of the current item.
*/
public int getIndex()
{
return count - 1;
}
/**
* @return the next element that meets the set {@link ActionCondition}s
* (if any) in the iterator being managed. If there are none left, then
* this will throw a {@link NoSuchElementException}.
*/
public Object next()
{
// if no next is cached...
if (this.next == null)
{
// try to cache one
if (!cacheNext(true))
{
// naughty! calling next() without knowing if there is one!
throw new NoSuchElementException("There are no more valid elements in this iterator");
}
}
// if we haven't returned any elements, first = true
if (first == null)
{
first = Boolean.TRUE;
}
// or if we've only returned one, first = false
else if (first.booleanValue())
{
first = Boolean.FALSE;
}
// update the number of iterations made
count++;
// get the cached next value
Object value = this.next;
// clear the cache
this.next = null;
// call next on synced ones
shiftSynced();
// return the no-longer-cached value
return value;
}
/**
* This operation is unsupported.
*/
public void remove()
{
// at this point, i don't see any use for this, so...
throw new UnsupportedOperationException("remove is not currently supported");
}
/**
* Stops this iterator from doing any further iteration.
*/
public void stop()
{
this.stopped = true;
this.next = null;
}
/**
* Directs this instance to completely exclude
* any elements equal to the specified Object.
* @param compare value to exclude
* @return This same {@link ManagedIterator} instance
*/
public ManagedIterator exclude(Object compare)
{
return condition(new ActionCondition(Action.EXCLUDE, new Equals(compare)));
}
/**
* Directs this instance to stop iterating immediately prior to
* any element equal to the specified Object.
* @param compare value to stop at
* @return This same {@link ManagedIterator} instance
*/
public ManagedIterator stop(Object compare)
{
return condition(new ActionCondition(Action.STOP, new Equals(compare)));
}
/**
* Adds a new {@link ActionCondition} for this instance to check
* against the elements in the iterator being managed.
* @param condition condition
* @return This same {@link ManagedIterator} instance
*/
public ManagedIterator condition(ActionCondition condition)
{
if (condition == null)
{
return null;
}
if (conditions == null)
{
conditions = new ArrayList<ActionCondition>();
}
conditions.add(condition);
return this;
}
/**
* <p>Adds another iterator to be kept in sync with the one
* being managed by this instance. The values of the parallel
* iterator can be retrieved from the LoopTool under the
* name s"synced" (e.g. $loop.synced or $loop.get('synced'))
* and are automatically updated for each iteration by this instance.
* </p><p><b>NOTE</b>: if you are sync'ing multiple iterators
* with the same managed iterator, you must use
* {@link #sync(Object,String)} or else your the later iterators
* will simply replace the earlier ones under the default
* 'synced' key.</p>
*
* @param iterable Iterator to synchronize with
* @return This same {@link ManagedIterator} instance
* @see SyncedIterator
* @see #get(String)
*/
public ManagedIterator sync(Object iterable)
{
return sync(iterable, "synced");
}
/**
* Adds another iterator to be kept in sync with the one
* being managed by this instance. The values of the parallel
* iterator can be retrieved from the LoopTool under the
* name specified here (e.g. $loop.name or $loop.get('name'))
* and are automatically updated for each iteration by this instance.
*
* @param iterable iterator to synchronize with
* @param name loop name
* @return This same {@link ManagedIterator} instance
* @see SyncedIterator
* @see #get(String)
*/
public ManagedIterator sync(Object iterable, String name)
{
Iterator parallel = getIterator(iterable);
if (parallel == null)
{
return null;
}
if (synced == null)
{
synced = new HashMap<String,SyncedIterator>();
}
synced.put(name, new SyncedIterator(parallel));
return this;
}
/**
* @return last sync'ed values
*/
public Map<String,Object> getLastSyncedValues()
{
if (synced == null)
{
return null;
}
Map<String,Object> syncs = new HashMap<String,Object>();
for (String key : synced.keySet())
{
syncs.put(key, synced.get(key).get());
}
return syncs;
}
@Override
public String toString()
{
return ManagedIterator.class.getSimpleName()+':'+getName();
}
}
/**
* Represents an automatic action taken by a {@link ManagedIterator}
* when a {@link Condition} is satisfied by the subsequent element.
*/
public static enum Action
{
EXCLUDE, STOP;
}
/**
* Composition class which associates an {@link Action} and {@link Condition}
* for a {@link ManagedIterator}.
*/
public static class ActionCondition
{
protected Condition condition;
protected Action action;
public ActionCondition(Action action, Condition condition)
{
if (condition == null || action == null)
{
throw new IllegalArgumentException("Condition and Action must both not be null");
}
this.condition = condition;
this.action = action;
}
/**
* Returns true if the specified value meets the set {@link Condition}
* @param value value to test
* @return whether the value matchers the condition
*/
public boolean matches(Object value)
{
return condition.test(value);
}
}
/**
* Represents a function into which a {@link ManagedIterator} can
* pass it's next element to see if an {@link Action} should be taken.
*/
public static interface Condition
{
public boolean test(Object value);
}
/**
* Base condition class for conditions (assumption here is that
* conditions are all comparative. Not much else makes sense to me
* for this context at this point.
*/
public static abstract class Comparison implements Condition
{
protected Object compare;
public Comparison(Object compare)
{
if (compare == null)
{
throw new IllegalArgumentException("Condition must have something to compare to");
}
this.compare = compare;
}
}
/**
* Simple condition that checks elements in the iterator
* for equality to a specified Object.
*/
public static class Equals extends Comparison
{
public Equals(Object compare)
{
super(compare);
}
public boolean test(Object value)
{
if (value == null)
{
return false;
}
if (compare.equals(value))
{
return true;
}
if (value.getClass().equals(compare.getClass()))
{
// no point in going on to string comparison
// if the classes are the same
return false;
}
// compare them as strings as a last resort
return String.valueOf(value).equals(String.valueOf(compare));
}
}
/**
* Simple wrapper to make it easy to keep an arbitray Iterator
* in sync with a {@link ManagedIterator}.
*/
public static class SyncedIterator
{
private Iterator iterator;
private Object current;
public SyncedIterator(Iterator iterator)
{
if (iterator == null)
{
// do we really care? perhaps we should just keep quiet...
throw new NullPointerException("Cannot synchronize a null Iterator");
}
this.iterator = iterator;
}
/**
* If the sync'ed iterator has any more values,
* this sets the next() value as the current one.
* If there are no more values, this sets the current
* one to {@code null}.
*/
public void shift()
{
if (iterator.hasNext())
{
current = iterator.next();
}
else
{
current = null;
}
}
/**
* @return the currently parallel value, if any.
*/
public Object get()
{
return current;
}
}
}