You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@velocity.apache.org by nb...@apache.org on 2008/01/10 03:34:15 UTC

svn commit: r610649 - in /velocity/tools/trunk: examples/showcase/loop.vm src/main/java/org/apache/velocity/tools/generic/LoopTool.java src/test/java/org/apache/velocity/tools/LoopToolTests.java

Author: nbubna
Date: Wed Jan  9 18:33:56 2008
New Revision: 610649

URL: http://svn.apache.org/viewvc?rev=610649&view=rev
Log:
add the ability to automatically stop before or exclude elements in a watched loop

Modified:
    velocity/tools/trunk/examples/showcase/loop.vm
    velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LoopTool.java
    velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LoopToolTests.java

Modified: velocity/tools/trunk/examples/showcase/loop.vm
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/examples/showcase/loop.vm?rev=610649&r1=610648&r2=610649&view=diff
==============================================================================
--- velocity/tools/trunk/examples/showcase/loop.vm (original)
+++ velocity/tools/trunk/examples/showcase/loop.vm Wed Jan  9 18:33:56 2008
@@ -21,7 +21,8 @@
 This tool is a convenience tool to use with ${esc.h}foreach loops. It wraps a
 list to let you prematurely end stop iterating, skip iterations, easily
 determine if you are on the first or last iteration, get the number of iterations
-completed, and even easily do all the above with complex, nested loops.
+completed, automatically exclude or break on particular elements and easily
+do all the above with complex, nested loops.
 </p>
 
 <form method="post" action="$link.self">
@@ -32,11 +33,10 @@
 ${esc.h}set( ${esc.d}numbers = [1, 2, 3, 4, 5, 6] )
 ${esc.h}set( ${esc.d}letters = ['A','B','C'] )
 ${esc.h}foreach( ${esc.d}l in ${esc.d}loop.watch(${esc.d}letters, 'l') )
-${esc.h}foreach( ${esc.d}n in ${esc.d}loop.watch(${esc.d}numbers) )
+${esc.h}foreach( ${esc.d}n in ${esc.d}loop.watch(${esc.d}numbers).exclude(3) )
 ${esc.h}if( ${esc.d}loop.first )${esc.d}l[${esc.h}end${esc.h}${esc.h}
 ${esc.d}n${esc.h}${esc.h}
 ${esc.h}if( ${esc.d}loop.last )]  ${esc.h}else, ${esc.h}end${esc.h}${esc.h}
-${esc.h}if( ${esc.d}n > 1 )${esc.d}loop.skip(1)${esc.h}end${esc.h}${esc.h} skip ahead each number over 1
 ${esc.h}if( ${esc.d}l == 'B' )${esc.d}loop.stop('l')${esc.h}end${esc.h}${esc.h} stop the letter loop after B
 ${esc.h}end
 ${esc.h}end##

Modified: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LoopTool.java
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LoopTool.java?rev=610649&r1=610648&r2=610649&view=diff
==============================================================================
--- velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LoopTool.java (original)
+++ velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/LoopTool.java Wed Jan  9 18:33:56 2008
@@ -19,7 +19,10 @@
  * under the License.
  */
 
+import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
 import java.util.Stack;
 import org.apache.velocity.tools.ClassUtils;
 import org.apache.velocity.tools.Scope;
@@ -96,7 +99,7 @@
      * @param obj an object that Velocity's #foreach directive can iterate over
      * @return a {@link ManagedIterator} that this tool instance will track
      */
-    public Iterator watch(Object obj)
+    public ManagedIterator watch(Object obj)
     {
         Iterator iterator = getIterator(obj);
         if (iterator == null)
@@ -119,7 +122,7 @@
      * be {@code null}.
      * @see #watch(Object)
      */
-    public Iterator watch(Object obj, String name)
+    public ManagedIterator watch(Object obj, String name)
     {
         // don't mess around with null names
         if (name == null)
@@ -231,9 +234,13 @@
         }
     }
 
+
     /**
-     * This tells the current loop to skip ahead the specified number of
-     * iterations before doing the next iteration.
+     * 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()}.
      */
     public void skip(int number)
     {
@@ -241,13 +248,13 @@
         if (!iterators.empty())
         {
             // tell the top one to skip the specified number
-            iterators.peek().skip(number);
+            skip(number, iterators.peek());
         }
     }
 
     /**
      * This tells the specified loop to skip ahead the specified number of
-     * iterations before doing the next iteration.
+     * iterations.
      * @see #skip(int)
      */
     public void skip(int number, String name)
@@ -256,7 +263,23 @@
         ManagedIterator iterator = findIterator(name);
         if (iterator != null)
         {
-            iterator.skip(number);
+            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;
+            }
         }
     }
 
@@ -362,6 +385,15 @@
         return null;
     }
 
+    /**
+     * Returns the number of loops currently on the stack to tell
+     * how deeply nested the current loop is.
+     */
+    public int getDepth()
+    {
+        return iterators.size();
+    }
+
 
     /**
      * Finds the {@link ManagedIterator} with the specified name
@@ -418,8 +450,11 @@
      * 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.
      */
-    protected static class ManagedIterator implements Iterator
+    public static class ManagedIterator implements Iterator
     {
         private String name;
         private Iterator iterator;
@@ -427,10 +462,12 @@
         private boolean stopped = false;
         private Boolean first = null;
         private int count = 0;
+        private Object next;
+        private List<ActionCondition> conditions;
 
         public ManagedIterator(Iterator iterator, LoopTool owner)
         {
-            this(iterator.toString(), iterator, owner);
+            this("loop"+owner.getDepth(), iterator, owner);
         }
 
         public ManagedIterator(String name, Iterator iterator, LoopTool owner)
@@ -444,11 +481,18 @@
             this.owner = owner;
         }
 
+        /**
+         * Returns the name of this instance.
+         */
         public String getName()
         {
             return this.name;
         }
 
+        /**
+         * Returns true if either 0 or 1 elements have been returned
+         * by {@link #next()}.
+         */
         public boolean isFirst()
         {
             if (first == null || first.booleanValue())
@@ -458,80 +502,296 @@
             return false;
         }
 
+        /**
+         * Returns 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 !iterator.hasNext();
+            return !hasNext(false);
         }
 
+        /**
+         * Returns 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()
         {
-            if (!stopped)
+            return hasNext(true);
+        }
+
+        // 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)
             {
-                boolean hasNext = iterator.hasNext();
-                // once this iterator is done, pop it from the owner's stack
-                if (!hasNext)
+                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
-                    stopped = true;
+                    stop();
                 }
-                return hasNext;
+                return false;
             }
-            else // if stopped
+
+            // ok, the iterator has more, but do they work for us?
+            this.next = iterator.next();
+            if (conditions != null)
             {
-                return false;
+                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");
+                        }
+                    }
+                }
             }
+
+            // ok, looks like we have a next that met all the conditions
+            return true;
         }
 
+        /**
+         * Returns the number of elements returned by {@link #next()} so far.
+         */
         public int getCount()
         {
             return count;
         }
 
+        /**
+         * Returns 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++;
-            return iterator.next();
+
+            // get the cached next value
+            Object value = this.next;
+            // clear the cache
+            this.next = null;
+            // return the no-longer-cached value
+            return value;
         }
 
+        /**
+         * This operation is unsupported.
+         */
         public void remove()
         {
-            // Let the iterator decide whether to implement this or not
-            this.iterator.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;
         }
 
-        public void skip(int number)
+        /**
+         * Directs this instance to completely exclude
+         * any elements equal to the specified Object.
+         * @return This same {@link ManagedIterator} instance
+         */
+        public ManagedIterator exclude(Object compare)
         {
-            for (int i=0; i < number; i++)
+            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.
+         * @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.
+         * @return This same {@link ManagedIterator} instance
+         */
+        public ManagedIterator condition(ActionCondition condition)
+        {
+            if (condition == null)
             {
-                if (iterator.hasNext())
-                {
-                    next();
-                }
-                else
-                {
-                    break;
-                }
+                return null;
+            }
+            if (conditions == null)
+            {
+                conditions = new ArrayList<ActionCondition>();
             }
+            conditions.add(condition);
+            return this;
         }
 
         @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}
+         */
+        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));
         }
     }
 

Modified: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LoopToolTests.java
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LoopToolTests.java?rev=610649&r1=610648&r2=610649&view=diff
==============================================================================
--- velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LoopToolTests.java (original)
+++ velocity/tools/trunk/src/test/java/org/apache/velocity/tools/LoopToolTests.java Wed Jan  9 18:33:56 2008
@@ -304,5 +304,38 @@
         assertEquals(loop.isLast(), loop.getLast());
     }
 
+    public @Test void watchAndExclude() throws Exception
+    {
+        LoopTool loop = new LoopTool();
+        Iterator i = loop.watch(ARRAY).exclude("bar");
+        assertEquals(i.next(), "foo");
+        assertEquals(i.next(), "woogie");
+        assertTrue(loop.isLast());
+        assertFalse(i.hasNext());
+    }
+
+    public @Test void watchAndStop() throws Exception
+    {
+        LoopTool loop = new LoopTool();
+        Iterator i = loop.watch(ARRAY).stop("bar");
+        assertEquals(i.next(), "foo");
+        assertTrue(loop.isLast());
+        assertFalse(i.hasNext());
+    }
+
+    public @Test void method_getDepth() throws Exception
+    {
+        LoopTool loop = new LoopTool();
+        assertEquals(0, loop.getDepth());
+        loop.watch(ARRAY);
+        assertEquals(1, loop.getDepth());
+        loop.watch(ARRAY);
+        assertEquals(2, loop.getDepth());
+        loop.pop();
+        assertEquals(1, loop.getDepth());
+        loop.pop();
+        assertEquals(0, loop.getDepth());
+    }
+
 }