View Javadoc

1   package org.apache.velocity.tools.generic;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.NoSuchElementException;
28  import java.util.Stack;
29  import org.apache.velocity.tools.ClassUtils;
30  import org.apache.velocity.tools.Scope;
31  import org.apache.velocity.tools.config.DefaultKey;
32  import org.apache.velocity.tools.config.ValidScope;
33  
34  /**
35   * <p>
36   * A convenience tool to use with #foreach loops. It wraps a list
37   * with a custom iterator to provide additional controls and feedback
38   * for managing loops.
39   * </p>
40   * <p>
41   * This tool was originally inspired the now-deprecated IteratorTool,
42   * which provided similar base functionality but was somewhat more difficult
43   * to understand and use.  Rather than try to migrate that implementation
44   * via deprecation and new methods, it was simplest to just create an
45   * entirely new tool that simplified the original API and was easy
46   * to augment with useful new features like support for nested 
47   * (and nameable) loops, skipping ahead in loops, synchronizing multiple
48   * iterators, getting the iteration count of loops, identifying if a loop is
49   * on its first or last iteration, and so on.
50   * </p>
51   * <p>
52   * Most functions of this tool will be obsolete with the release of
53   * Velocity 1.7, which will provide $foreach.hasNext, $foreach.isFirst,
54   * $foreach.isLast, $foreach.index and $foreach.count automatically.
55   * However, this will still be useful for the more advanced sync
56   * and skip features.  Also, for very complicated nested loops, the
57   * loop naming feature may be easier than doing things like $foreach.parent.parent.
58   * </p>
59   * <p>
60   * Example of use:
61   * <pre>
62   *  Template
63   *  ---
64   *  #set( $list = [1..7] )
65   *  #set( $others = [3..10] )
66   *  #foreach( $item in $loop.watch($list).sync($others, 'other') )
67   *  $item -> $loop.other
68   *  #if( $item >= 5 )$loop.stop()#end
69   *  #end
70   *
71   *  Output
72   *  ------
73   *  1 -> 3
74   *  2 -> 4
75   *  3 -> 5
76   *  4 -> 6
77   *  5 -> 7
78   *
79   * Example tools.xml config (if you want to use this with VelocityView):
80   * &lt;tools&gt;
81   *   &lt;toolbox scope="request"&gt;
82   *     &lt;tool class="org.apache.velocity.tools.generic.LoopTool"/&gt;
83   *   &lt;/toolbox&gt;
84   * &lt;/tools&gt;
85   * </pre>
86   * </p>
87   *
88   * @author Nathan Bubna
89   * @version $Id: LoopTool.java 590893 2007-11-01 04:40:21Z nbubna $
90   */
91  @DefaultKey("loop")
92  @ValidScope(Scope.REQUEST)
93  public class LoopTool
94  {
95      private Stack<ManagedIterator> iterators = new Stack<ManagedIterator>();
96      private ManagedIterator last;
97  
98      /**
99       * <p>Tells the LoopTool to watch the specified Array, Collection, Map,
100      * Iterator, Iterable, Enumeration or POJO with an iterator() method
101      * while the template iterates over the values within it.
102      * </p>
103      * <p>Under the covers, this is returning an iterable wrapper that
104      * is also pushed onto this tool's stack.  That allows this tool to
105      * know which iterator to give later commands (i.e. stop() or skip()).
106      * </p>
107      * @param obj an object that Velocity's #foreach directive can iterate over
108      * @return a {@link ManagedIterator} that this tool instance will track
109      */
110     public ManagedIterator watch(Object obj)
111     {
112         Iterator iterator = getIterator(obj);
113         if (iterator == null)
114         {
115             return null;
116         }
117 
118         ManagedIterator managed = manage(iterator, null);
119         iterators.push(managed);
120         this.last = managed;
121         return managed;
122     }
123 
124     /**
125      * This is just like {@link #watch(Object)} except that it also takes
126      * a name which is given to the {@link ManagedIterator} that is returned.
127      * This allows the user to send stop or skip commands to that specific
128      * iterator even when there are nested iterators within it that are being
129      * watched.  If the given name is {@code null}, then this will return 
130      * {@code null} even if the object can be watched. Provided names cannot
131      * be {@code null}.
132      * @see #watch(Object)
133      */
134     public ManagedIterator watch(Object obj, String name)
135     {
136         // don't mess around with null names
137         if (name == null)
138         {
139             return null;
140         }
141         Iterator iterator = getIterator(obj);
142         if (iterator == null)
143         {
144             return null;
145         }
146 
147         ManagedIterator managed = manage(iterator, name);
148         iterators.push(managed);
149         this.last = managed;
150         return managed;
151     }
152 
153     public ManagedIterator sync(Object main, Object synced)
154     {
155         return watch(main).sync(synced);
156     }
157 
158     protected ManagedIterator manage(Iterator iterator, String name)
159     {
160         return new ManagedIterator(name, iterator, this);
161     }
162 
163     /**
164      * This tells the current loop to stop after the current iteration.
165      * This is different from "break" common to most programming languages,
166      * in that it does not immediately cease activity in the current iteration.
167      * Instead, it merely tells the #foreach loop that this is the last time
168      * around.
169      */
170     public void stop()
171     {
172         // if we have an iterator on the stack
173         if (!iterators.empty())
174         {
175             // stop the top one, so #foreach doesn't loop again
176             iterators.peek().stop();
177         }
178     }
179 
180     /**
181      * This is just like {@link #stop()} except that the stop command is issued
182      * <strong>only</strong> to the loop/iterator with the specified name.
183      * If no such loop is found with that name, then no stop command is issued.
184      * @see #stop()
185      */
186     public void stop(String name)
187     {
188         // just stop the matching one
189         for (ManagedIterator iterator : iterators)
190         {
191             if (iterator.getName().equals(name))
192             {
193                 iterator.stop();
194                 break;
195             }
196         }
197     }
198 
199     /**
200      * This is just like {@link #stop(String)} except that the stop command is issued
201      * both to the loop/iterator with the specified name and all loops nested within
202      * it.  If no such loop is found with that name, then no stop commands are
203      * issued.
204      * @see #stop()
205      * @see #stop(String)
206      */
207     public void stopTo(String name)
208     {
209         if (!iterators.empty())
210         {
211             // create a backup stack to put things back as they were
212             Stack<ManagedIterator> backup = new Stack<ManagedIterator>();
213             // look for the iterator with the specified name
214             boolean found = false;
215             while (!found && !iterators.empty())
216             {
217                 ManagedIterator iterator = iterators.pop();
218                 if (iterator.getName().equals(name))
219                 {
220                     found = true;
221                     iterator.stop();
222                 }
223                 else
224                 {
225                     // keep a backup of the ones that don't match
226                     backup.push(iterator);
227                 }
228             }
229 
230             while (!backup.empty())
231             {
232                 // push the nested iterators back
233                 ManagedIterator iterator = backup.pop();
234                 iterators.push(iterator);
235                 if (found)
236                 {
237                     iterator.stop();
238                 }
239             }
240         }
241     }
242 
243     /**
244      * This is just like {@link #stop()} except that the stop command is issued
245      * <strong>all</strong> the loops being watched by this tool.
246      * @see #stop()
247      */
248     public void stopAll()
249     {
250         // just stop them all
251         for (ManagedIterator iterator : iterators)
252         {
253             iterator.stop();
254         }
255     }
256 
257 
258     /**
259      * Skips ahead the specified number of iterations (if possible).
260      * Since this is manual skipping (unlike the automatic skipping
261      * provided by the likes of {@link ManagedIterator#exclude(Object)}, any elements
262      * skipped are still considered in the results returned by {@link #getCount()}
263      * and {@link #isFirst()}.
264      */
265     public void skip(int number)
266     {
267         // if we have an iterator on the stack
268         if (!iterators.empty())
269         {
270             // tell the top one to skip the specified number
271             skip(number, iterators.peek());
272         }
273     }
274 
275     /**
276      * This tells the specified loop to skip ahead the specified number of
277      * iterations.
278      * @see #skip(int)
279      */
280     public void skip(int number, String name)
281     {
282         // just tell the matching one to skip
283         ManagedIterator iterator = findIterator(name);
284         if (iterator != null)
285         {
286             skip(number, iterator);
287         }
288     }
289 
290     // does the actual skipping by manually advancing the ManagedIterator
291     private void skip(int number, ManagedIterator iterator)
292     {
293         for (int i=0; i < number; i++)
294         {
295             if (iterator.hasNext())
296             {
297                 iterator.next();
298             }
299             else
300             {
301                 break;
302             }
303         }
304     }
305 
306     /**
307      * Returns {@code true} if the current loop is on its first iteration.
308      */
309     public Boolean isFirst()
310     {
311         if (last != null)
312         {
313             return last.isFirst();
314         }
315         return null;
316     }
317 
318     /**
319      * Returns {@code true} if the loop with the specified name
320      * is on its first iteration.
321      */
322     public Boolean isFirst(String name)
323     {
324         // just tell the matching one to skip
325         ManagedIterator iterator = findIterator(name);
326         if (iterator != null)
327         {
328             return iterator.isFirst();
329         }
330         return null;
331     }
332 
333     /**
334      * Returns the result of {@link #isFirst}. Exists to allow $loop.first syntax.
335      */
336     public Boolean getFirst()
337     {
338         return isFirst();
339     }
340 
341     /**
342      * Returns {@code true} if the current loop is on its last iteration.
343      */
344     public Boolean isLast()
345     {
346         if (last != null)
347         {
348             return last.isLast();
349         }
350         return null;
351     }
352 
353     /**
354      * Returns {@code true} if the loop with the specified name
355      * is on its last iteration.
356      */
357     public Boolean isLast(String name)
358     {
359         // just tell the matching one to skip
360         ManagedIterator iterator = findIterator(name);
361         if (iterator != null)
362         {
363             return iterator.isLast();
364         }
365         return null;
366     }
367 
368     /**
369      * Returns the result of {@link #isLast}. Exists to allow $loop.last syntax.
370      */
371     public Boolean getLast()
372     {
373         return isLast();
374     }
375 
376     /**
377      * <p>This serves two purposes:
378      * <ul><li>Getting the current value of a sync'ed iterator</li>
379      * <li>Abbreviate syntax for properties of outer loops</li></ul></p>
380      * <p>First, it searches all the loops being managed for one
381      * with a sync'ed Iterator under the specified name and
382      * returns the current value for that sync'ed iterator,
383      * if any. If there is no sync'ed iterators or none with
384      * that name, then this will check if the specified key
385      * is requesting a "property" of an outer loop (e.g.
386      * {@code $loop.count_foo} or {@code $loop.first_foo}).
387      * This syntax is shorter and clearer than {@code $loop.getCount('foo')}.
388      * If the key starts with a property name and ends with an outer loop
389      * name, then the value of that property for that loop is returned.
390      */
391     public Object get(String key)
392     {
393         // search all iterators in reverse
394         // (so nested ones take priority)
395         // for one that is responsible for synced
396         for (int i=iterators.size() - 1; i >= 0; i--)
397         {
398             ManagedIterator iterator = iterators.get(i);
399             if (iterator.isSyncedWith(key))
400             {
401                 return iterator.get(key);
402             }
403         }
404         // shortest key would be "last_X" where X is the loop name
405         if (key == null || key.length() < 6)
406         {
407             return null;
408         }
409         if (key.startsWith("last_"))
410         {
411             return isLast(key.substring(5, key.length()));
412         }
413         if (key.startsWith("count_"))
414         {
415             return getCount(key.substring(6, key.length()));
416         }
417         if (key.startsWith("index_"))
418         {
419             return getIndex(key.substring(6, key.length()));
420         }
421         if (key.startsWith("first_"))
422         {
423             return isFirst(key.substring(6, key.length()));
424         }
425         return null;
426     }
427 
428     /**
429      * Asks the loop with the specified name for the current value
430      * of the specified sync'ed iterator, if any.
431      */
432     public Object get(String name, String synced)
433     {
434         // just ask the matching iterator for the sync'ed value
435         ManagedIterator iterator = findIterator(name);
436         if (iterator != null)
437         {
438             return iterator.get(synced);
439         }
440         return null;
441     }
442 
443     /**
444      * Returns the 0-based index of the item the current loop is handling.
445      * So, if this is the first iteration, then the index will be 0. If
446      * you {@link #skip} ahead in this loop, those skipped iterations will
447      * still be reflected in the index.  If iteration has not begun, this
448      * will return {@code null}.
449      */
450     public Integer getIndex()
451     {
452         Integer count = getCount();
453         if (count == null || count == 0)
454         {
455             return null;
456         }
457         return count - 1;
458     }
459 
460     /**
461      * Returns the 0-based index of the item the specified loop is handling.
462      * So, if this is the first iteration, then the index will be 0. If
463      * you {@link #skip} ahead in this loop, those skipped iterations will
464      * still be reflected in the index.  If iteration has not begun, this
465      * will return {@code null}.
466      */
467     public Integer getIndex(String name)
468     {
469         Integer count = getCount(name);
470         if (count == null || count == 0)
471         {
472             return null;
473         }
474         return count - 1;
475     }
476 
477     /**
478      * Returns the number of items the current loop has handled. So, if this
479      * is the first iteration, then the count will be 1.  If you {@link #skip}
480      * ahead in this loop, those skipped iterations will still be included in
481      * the count.
482      */
483     public Integer getCount()
484     {
485         if (last != null)
486         {
487             return last.getCount();
488         }
489         return null;
490     }
491 
492     /**
493      * Returns the number of items the specified loop has handled. So, if this
494      * is the first iteration, then the count will be 1.  If you {@link #skip}
495      * ahead in this loop, those skipped iterations will still be included in
496      * the count.
497      */
498     public Integer getCount(String name)
499     {
500         // just tell the matching one to skip
501         ManagedIterator iterator = findIterator(name);
502         if (iterator != null)
503         {
504             return iterator.getCount();
505         }
506         return null;
507     }
508 
509     /**
510      * Returns the most recent {@link ManagedIterator} for this instance.
511      * This can be used to access properties like the count, index,
512      * isFirst, isLast, etc which would otherwise fail on the last item
513      * in a loop due to the necessity of popping iterators off the
514      * stack when the last item is retrieved. (See VELTOOLS-124)
515      */
516     public ManagedIterator getThis()
517     {
518         return last;
519     }
520 
521     /**
522      * Returns the number of loops currently on the stack.
523      * This is only useful for debugging, as iterators are
524      * popped off the stack at the start of their final iteration,
525      * making this frequently "incorrect".
526      */
527     public int getDepth()
528     {
529         return iterators.size();
530     }
531 
532 
533     /**
534      * Finds the {@link ManagedIterator} with the specified name
535      * if it is in this instance's iterator stack.
536      */
537     protected ManagedIterator findIterator(String name)
538     {
539         // look for the one with the specified name
540         for (ManagedIterator iterator : iterators)
541         {
542             if (iterator.getName().equals(name))
543             {
544                 return iterator;
545             }
546         }
547         return null;
548     }
549 
550     /**
551      * Don't let templates call this, but allow subclasses
552      * and ManagedIterator to have access.
553      */
554     protected ManagedIterator pop()
555     {
556         return iterators.pop();
557     }
558 
559 
560     /**
561      * Wraps access to {@link ClassUtils#getIterator} is a 
562      * nice little try/catch block to prevent exceptions from
563      * escaping into the template.  In the case of such problems,
564      * this will return {@code null}.
565      */
566     protected static Iterator getIterator(Object obj)
567     {
568         if (obj == null)
569         {
570             return null;
571         }
572         try
573         {
574             return ClassUtils.getIterator(obj);
575         }
576         catch (Exception e)
577         {
578             //TODO: pick up a log so we can log this
579         }
580         return null;
581     }
582 
583 
584     /**
585      * Iterator implementation that wraps a standard {@link Iterator}
586      * and allows it to be prematurely stopped, skipped ahead, and 
587      * associated with a name for advanced nested loop control.
588      * This also allows a arbitrary {@link ActionCondition}s to be added
589      * in order to have it automatically skip over or stop before
590      * certain elements in the iterator.
591      */
592     public static class ManagedIterator implements Iterator
593     {
594         private String name;
595         private Iterator iterator;
596         private LoopTool owner;
597         private boolean stopped = false;
598         private Boolean first = null;
599         private int count = 0;
600         private Object next;
601         private List<ActionCondition> conditions;
602         private Map<String,SyncedIterator> synced;
603 
604         public ManagedIterator(String name, Iterator iterator, LoopTool owner)
605         {
606             if (name == null)
607             {
608                 this.name = "loop"+owner.getDepth();
609             }
610             else
611             {
612                 this.name = name;
613             }
614             this.iterator = iterator;
615             this.owner = owner;
616         }
617 
618         /**
619          * Returns the name of this instance.
620          */
621         public String getName()
622         {
623             return this.name;
624         }
625 
626         /**
627          * Returns true if either 0 or 1 elements have been returned
628          * by {@link #next()}.
629          */
630         public boolean isFirst()
631         {
632             if (first == null || first.booleanValue())
633             {
634                 return true;
635             }
636             return false;
637         }
638 
639         /**
640          * Returns true if the last element returned by {@link #next()}
641          * is the last element available in the iterator being managed
642          * which satisfies any/all {@link ActionCondition}s set for this
643          * instance. Otherwise, returns false.
644          */
645         public boolean isLast()
646         {
647             return !hasNext(false);
648         }
649 
650         /**
651          * Returns the result of {@link #isFirst}. Exists to allow $loop.this.first syntax.
652          */
653         public boolean getFirst()
654         {
655             return isFirst();
656         }
657 
658         /**
659          * Returns the result of {@link #isLast}. Exists to allow $loop.this.last syntax.
660          */
661         public boolean getLast()
662         {
663             return isLast();
664         }
665 
666         /**
667          * Returns true if there are more elements in the iterator
668          * being managed by this instance which satisfy all the
669          * {@link ActionCondition}s set for this instance.  Returns
670          * false if there are no more valid elements available.
671          */
672         public boolean hasNext()
673         {
674             return hasNext(true);
675         }
676 
677         /**
678          * Returns the result of {@link #hasNext}. Exists to allow $loop.this.hasNext syntax.
679          */
680         public boolean getHasNext()
681         {
682             return hasNext(false);//no need to pop, #foreach will always call hasNext()
683         }
684 
685         // version that lets isLast check w/o popping this from the stack
686         private boolean hasNext(boolean popWhenDone)
687         {
688             // we don't if we've stopped
689             if (stopped)
690             {
691                 return false;
692             }
693             // we're not stopped, so do we have a next cached?
694             if (this.next != null)
695             {
696                 return true;
697             }
698             // try to get a next that satisfies the conditions
699             // if there isn't one, return false; if there is, return true
700             return cacheNext(popWhenDone);
701         }
702 
703         // Tries to get a next that satisfies the conditions.
704         // Returns true if there is a next to get.
705         private boolean cacheNext(boolean popWhenDone)
706         {
707             // ok, let's see if we can get a next
708             if (!iterator.hasNext())
709             {
710                 if (popWhenDone)
711                 {
712                     // this iterator is done, pop it from the owner's stack
713                     owner.pop();
714                     // and make sure we don't pop twice
715                     stop();
716                 }
717                 return false;
718             }
719 
720             // ok, the iterator has more, but do they work for us?
721             this.next = iterator.next();
722             if (conditions != null)
723             {
724                 for (ActionCondition condition : conditions)
725                 {
726                     if (condition.matches(this.next))
727                     {
728                         switch (condition.action)
729                         {
730                             case EXCLUDE:
731                                 // recurse on to the next one
732                                 return cacheNext(popWhenDone);
733                             case STOP:
734                                 stop();
735                                 return false;
736                             default:
737                                 throw new IllegalStateException("ActionConditions should never have a null Action");
738                         }
739                     }
740                 }
741             }
742 
743             // ok, looks like we have a next that met all the conditions
744             shiftSynced();
745             return true;
746         }
747 
748         private void shiftSynced()
749         {
750             if (synced != null)
751             {
752                 for (SyncedIterator parallel : synced.values())
753                 {
754                     parallel.shift();
755                 }
756             }
757         }
758 
759         /**
760          * Returns {@code true} if this ManagedIterator has a sync'ed
761          * iterator with the specified name.
762          */
763         public boolean isSyncedWith(String name)
764         {
765             if (synced == null)
766             {
767                 return false;
768             }
769             return synced.containsKey(name);
770         }
771 
772         /**
773          * Returns the parallel value from the specified sync'ed iterator.
774          * If no sync'ed iterator exists with that name or that iterator
775          * is finished, this will return {@code null}.
776          */
777         public Object get(String name)
778         {
779             if (synced == null)
780             {
781                 return null;
782             }
783             SyncedIterator parallel = synced.get(name);
784             if (parallel == null)
785             {
786                 return null;
787             }
788             return parallel.get();
789         }
790 
791         /**
792          * Returns the number of elements returned by {@link #next()} so far.
793          */
794         public int getCount()
795         {
796             return count;
797         }
798 
799         /**
800          * Returns the 0-based index of the current item.
801          */
802         public int getIndex()
803         {
804             return count - 1;
805         }
806 
807         /**
808          * Returns the next element that meets the set {@link ActionCondition}s
809          * (if any) in the iterator being managed. If there are none left, then
810          * this will throw a {@link NoSuchElementException}.
811          */
812         public Object next()
813         {
814             // if no next is cached...
815             if (this.next == null)
816             {
817                 // try to cache one
818                 if (!cacheNext(true))
819                 {
820                     // naughty! calling next() without knowing if there is one!
821                     throw new NoSuchElementException("There are no more valid elements in this iterator");
822                 }
823             }
824 
825             // if we haven't returned any elements, first = true
826             if (first == null)
827             {
828                 first = Boolean.TRUE;
829             }
830             // or if we've only returned one, first = false
831             else if (first.booleanValue())
832             {
833                 first = Boolean.FALSE;
834             }
835             // update the number of iterations made
836             count++;
837 
838             // get the cached next value
839             Object value = this.next;
840             // clear the cache
841             this.next = null;
842             // return the no-longer-cached value
843             return value;
844         }
845 
846         /**
847          * This operation is unsupported.
848          */
849         public void remove()
850         {
851             // at this point, i don't see any use for this, so...
852             throw new UnsupportedOperationException("remove is not currently supported");
853         }
854 
855         /**
856          * Stops this iterator from doing any further iteration.
857          */
858         public void stop()
859         {
860             this.stopped = true;
861             this.next = null;
862         }
863 
864         /**
865          * Directs this instance to completely exclude
866          * any elements equal to the specified Object.
867          * @return This same {@link ManagedIterator} instance
868          */
869         public ManagedIterator exclude(Object compare)
870         {
871             return condition(new ActionCondition(Action.EXCLUDE, new Equals(compare)));
872         }
873 
874 
875         /**
876          * Directs this instance to stop iterating immediately prior to
877          * any element equal to the specified Object.
878          * @return This same {@link ManagedIterator} instance
879          */
880         public ManagedIterator stop(Object compare)
881         {
882             return condition(new ActionCondition(Action.STOP, new Equals(compare)));
883         }
884 
885         /**
886          * Adds a new {@link ActionCondition} for this instance to check
887          * against the elements in the iterator being managed.
888          * @return This same {@link ManagedIterator} instance
889          */
890         public ManagedIterator condition(ActionCondition condition)
891         {
892             if (condition == null)
893             {
894                 return null;
895             }
896             if (conditions == null)
897             {
898                 conditions = new ArrayList<ActionCondition>();
899             }
900             conditions.add(condition);
901             return this;
902         }
903 
904         /**
905          * <p>Adds another iterator to be kept in sync with the one
906          * being managed by this instance.  The values of the parallel
907          * iterator can be retrieved from the LoopTool under the
908          * name s"synced" (e.g. $loop.synched or $loop.get('synced'))
909          * and are automatically updated for each iteration by this instance.
910          * </p><p><b>NOTE</b>: if you are sync'ing multiple iterators
911          * with the same managed iterator, you must use 
912          * {@link #sync(Object,String)} or else your the later iterators
913          * will simply replace the earlier ones under the default
914          * 'synced' key.</p>
915          *
916          * @return This same {@link ManagedIterator} instance
917          * @see SyncedIterator
918          * @see #get(String)
919          */
920         public ManagedIterator sync(Object iterable)
921         {
922             return sync(iterable, "synced");
923         }
924 
925         /**
926          * Adds another iterator to be kept in sync with the one
927          * being managed by this instance.  The values of the parallel
928          * iterator can be retrieved from the LoopTool under the
929          * name specified here (e.g. $loop.name or $loop.get('name'))
930          * and are automatically updated for each iteration by this instance.
931          *
932          * @return This same {@link ManagedIterator} instance
933          * @see SyncedIterator
934          * @see #get(String)
935          */
936         public ManagedIterator sync(Object iterable, String name)
937         {
938             Iterator parallel = LoopTool.getIterator(iterable);
939             if (parallel == null)
940             {
941                 return null;
942             }
943             if (synced == null)
944             {
945                 synced = new HashMap<String,SyncedIterator>();
946             }
947             synced.put(name, new SyncedIterator(parallel));
948             return this;
949         }
950 
951         @Override
952         public String toString()
953         {
954             return ManagedIterator.class.getSimpleName()+':'+getName();
955         }
956     }
957 
958     /**
959      * Represents an automatic action taken by a {@link ManagedIterator}
960      * when a {@link Condition} is satisfied by the subsequent element.
961      */
962     public static enum Action
963     {
964         EXCLUDE, STOP;
965     }
966 
967     /**
968      * Composition class which associates an {@link Action} and {@link Condition}
969      * for a {@link ManagedIterator}.
970      */
971     public static class ActionCondition
972     {
973         protected Condition condition;
974         protected Action action;
975 
976         public ActionCondition(Action action, Condition condition)
977         {
978             if (condition == null || action == null)
979             {
980                 throw new IllegalArgumentException("Condition and Action must both not be null");
981             }
982             this.condition = condition;
983             this.action = action;
984         }
985 
986         /**
987          * Returns true if the specified value meets the set {@link Condition}
988          */
989         public boolean matches(Object value)
990         {
991             return condition.test(value);
992         }
993     }
994 
995     /**
996      * Represents a function into which a {@link ManagedIterator} can
997      * pass it's next element to see if an {@link Action} should be taken.
998      */
999     public static interface Condition
1000     {
1001         public boolean test(Object value);
1002     }
1003 
1004     /**
1005      * Base condition class for conditions (assumption here is that
1006      * conditions are all comparative.  Not much else makes sense to me
1007      * for this context at this point.
1008      */
1009     public static abstract class Comparison implements Condition
1010     {
1011         protected Object compare;
1012 
1013         public Comparison(Object compare)
1014         {
1015             if (compare == null)
1016             {
1017                 throw new IllegalArgumentException("Condition must have something to compare to");
1018             }
1019             this.compare = compare;
1020         }
1021     }
1022 
1023     /**
1024      * Simple condition that checks elements in the iterator
1025      * for equality to a specified Object.
1026      */
1027     public static class Equals extends Comparison
1028     {
1029         public Equals(Object compare)
1030         {
1031             super(compare);
1032         }
1033 
1034         public boolean test(Object value)
1035         {
1036             if (value == null)
1037             {
1038                 return false;
1039             }
1040             if (compare.equals(value))
1041             {
1042                 return true;
1043             }
1044             if (value.getClass().equals(compare.getClass()))
1045             {
1046                 // no point in going on to string comparison
1047                 // if the classes are the same
1048                 return false;
1049             }
1050             // compare them as strings as a last resort
1051             return String.valueOf(value).equals(String.valueOf(compare));
1052         }
1053     }
1054 
1055 
1056     /**
1057      * Simple wrapper to make it easy to keep an arbitray Iterator
1058      * in sync with a {@link ManagedIterator}.
1059      */
1060     public static class SyncedIterator
1061     {
1062         private Iterator iterator;
1063         private Object current;
1064 
1065         public SyncedIterator(Iterator iterator)
1066         {
1067             if (iterator == null)
1068             {
1069                 // do we really care?  perhaps we should just keep quiet...
1070                 throw new NullPointerException("Cannot synchronize a null Iterator");
1071             }
1072             this.iterator = iterator;
1073         }
1074 
1075         /**
1076          * If the sync'ed iterator has any more values,
1077          * this sets the next() value as the current one.
1078          * If there are no more values, this sets the current
1079          * one to {@code null}.
1080          */
1081         public void shift()
1082         {
1083             if (iterator.hasNext())
1084             {
1085                 current = iterator.next();
1086             }
1087             else
1088             {
1089                 current = null;
1090             }
1091         }
1092 
1093         /**
1094          * Returns the currently parallel value, if any.
1095          */
1096         public Object get()
1097         {
1098             return current;
1099         }
1100     }
1101 
1102 }