View Javadoc

1   package org.apache.velocity.runtime.directive;
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.io.IOException;
23  import java.io.Writer;
24  import java.util.Iterator;
25  
26  import org.apache.velocity.context.ChainedInternalContextAdapter;
27  import org.apache.velocity.context.InternalContextAdapter;
28  import org.apache.velocity.exception.MethodInvocationException;
29  import org.apache.velocity.exception.ParseErrorException;
30  import org.apache.velocity.exception.ResourceNotFoundException;
31  import org.apache.velocity.exception.TemplateInitException;
32  import org.apache.velocity.exception.VelocityException;
33  import org.apache.velocity.runtime.RuntimeConstants;
34  import org.apache.velocity.runtime.RuntimeServices;
35  import org.apache.velocity.runtime.log.Log;
36  import org.apache.velocity.runtime.parser.node.ASTReference;
37  import org.apache.velocity.runtime.parser.node.Node;
38  import org.apache.velocity.runtime.parser.node.SimpleNode;
39  import org.apache.velocity.util.introspection.Info;
40  
41  /**
42   * Foreach directive used for moving through arrays,
43   * or objects that provide an Iterator.
44   *
45   * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
46   * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
47   * @author Daniel Rall
48   * @version $Id: Foreach.java 730367 2008-12-31 10:29:21Z byron $
49   */
50  public class Foreach extends Directive
51  {
52      /**
53       * A special context to use when the foreach iterator returns a null.  This
54       * is required since the standard context may not support nulls.
55       * All puts and gets are passed through, except for the foreach iterator key.
56       * @since 1.5
57       */
58      protected static class NullHolderContext extends ChainedInternalContextAdapter
59      {
60          private String   loopVariableKey = "";
61          private boolean  active = true;
62  
63          /**
64           * Create the context as a wrapper to be used within the foreach
65           * @param key the reference used in the foreach
66           * @param context the parent context
67           */
68          private NullHolderContext( String key, InternalContextAdapter context )
69          {
70             super(context);
71             if( key != null )
72                 loopVariableKey = key;
73          }
74  
75          /**
76           * Get an object from the context, or null if the key is equal to the loop variable
77           * @see org.apache.velocity.context.InternalContextAdapter#get(java.lang.String)
78           * @exception MethodInvocationException passes on potential exception from reference method call
79           */
80          public Object get( String key ) throws MethodInvocationException
81          {
82              return ( active && loopVariableKey.equals(key) )
83                  ? null
84                  : super.get(key);
85          }
86  
87          /**
88           * @see org.apache.velocity.context.InternalContextAdapter#put(java.lang.String key, java.lang.Object value)
89           */
90          public Object put( String key, Object value )
91          {
92              if( loopVariableKey.equals(key) && (value == null) )
93              {
94                  active = true;
95              }
96  
97              return super.put( key, value );
98          }
99  
100         /**
101          * Allows callers to explicitly put objects in the local context.
102          * Objects added to the context through this method always end up
103          * in the top-level context of possible wrapped contexts.
104          *
105          * @param key name of item to set.
106          * @param value object to set to key.
107          * @see org.apache.velocity.context.InternalWrapperContext#localPut(String, Object)
108          */        
109         public Object localPut(final String key, final Object value)
110         {
111             return put(key, value);
112         }
113 
114         /**
115          * Remove an object from the context
116          * @see org.apache.velocity.context.InternalContextAdapter#remove(java.lang.Object key)
117          */
118         public Object remove(Object key)
119         {
120            if( loopVariableKey.equals(key) )
121            {
122              active = false;
123            }
124            return super.remove(key);
125         }
126     }
127 
128     /**
129      * Return name of this directive.
130      * @return The name of this directive.
131      */
132     public String getName()
133     {
134         return "foreach";
135     }
136 
137     /**
138      * Return type of this directive.
139      * @return The type of this directive.
140      */
141     public int getType()
142     {
143         return BLOCK;
144     }
145 
146     /**
147      * The name of the variable to use when placing
148      * the counter value into the context. Right
149      * now the default is $velocityCount.
150      */
151     private String counterName;
152 
153     /**
154      * The name of the variable to use when placing
155      * iterator hasNext() value into the context.Right
156      * now the defailt is $velocityHasNext
157      */
158     private String hasNextName;
159 
160     /**
161      * What value to start the loop counter at.
162      */
163     private int counterInitialValue;
164 
165     /**
166      * The maximum number of times we're allowed to loop.
167      */
168     private int maxNbrLoops;
169 
170     /**
171      * Whether or not to throw an Exception if the iterator is null.
172      */
173     private boolean skipInvalidIterator;
174 
175     /**
176      * The reference name used to access each
177      * of the elements in the list object. It
178      * is the $item in the following:
179      *
180      * #foreach ($item in $list)
181      *
182      * This can be used class wide because
183      * it is immutable.
184      */
185     private String elementKey;
186 
187     /**
188      *  immutable, so create in init
189      */
190     protected Info uberInfo;
191 
192     /**
193      *  simple init - init the tree and get the elementKey from
194      *  the AST
195      * @param rs
196      * @param context
197      * @param node
198      * @throws TemplateInitException
199      */
200     public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
201         throws TemplateInitException
202     {
203         super.init(rs, context, node);
204 
205         counterName = rsvc.getString(RuntimeConstants.COUNTER_NAME);
206         hasNextName = rsvc.getString(RuntimeConstants.HAS_NEXT_NAME);
207         counterInitialValue = rsvc.getInt(RuntimeConstants.COUNTER_INITIAL_VALUE);
208         maxNbrLoops = rsvc.getInt(RuntimeConstants.MAX_NUMBER_LOOPS,
209                                   Integer.MAX_VALUE);
210         if (maxNbrLoops < 1)
211         {
212             maxNbrLoops = Integer.MAX_VALUE;
213         }
214         skipInvalidIterator =
215             rsvc.getBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, true);
216         
217         if (rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false))
218         {
219           // If we are in strict mode then the default for skipInvalidItarator
220           // is true.  However, if the property is explicitly set, then honor the setting.
221           skipInvalidIterator = rsvc.getBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, false);
222         }
223                 
224         /*
225          *  this is really the only thing we can do here as everything
226          *  else is context sensitive
227          */
228         SimpleNode sn = (SimpleNode) node.jjtGetChild(0);
229 
230         if (sn instanceof ASTReference)
231         {
232             elementKey = ((ASTReference) sn).getRootString();
233         }
234         else
235         {
236             /*
237              * the default, error-prone way which we'll remove
238              *  TODO : remove if all goes well
239              */
240             elementKey = sn.getFirstToken().image.substring(1);
241         }
242 
243         /*
244          * make an uberinfo - saves new's later on
245          */
246 
247         uberInfo = new Info(this.getTemplateName(),
248                 getLine(),getColumn());
249     }
250 
251     /**
252      * Extension hook to allow subclasses to control whether loop vars
253      * are set locally or not. So, those in favor of VELOCITY-285, can
254      * make that happen easily by overriding this and having it use
255      * context.localPut(k,v). See VELOCITY-630 for more on this.
256      */
257     protected void put(InternalContextAdapter context, String key, Object value)
258     {
259         context.put(key, value);
260     }
261 
262     /**
263      *  renders the #foreach() block
264      * @param context
265      * @param writer
266      * @param node
267      * @return True if the directive rendered successfully.
268      * @throws IOException
269      * @throws MethodInvocationException
270      * @throws ResourceNotFoundException
271      * @throws ParseErrorException
272      */
273     public boolean render(InternalContextAdapter context,
274                            Writer writer, Node node)
275         throws IOException,  MethodInvocationException, ResourceNotFoundException,
276         	ParseErrorException
277     {
278         /*
279          *  do our introspection to see what our collection is
280          */
281 
282         Object listObject = node.jjtGetChild(2).value(context);
283 
284         if (listObject == null)
285              return false;
286 
287         Iterator i = null;
288 
289         try
290         {
291             i = rsvc.getUberspect().getIterator(listObject, uberInfo);
292         }
293         /**
294          * pass through application level runtime exceptions
295          */
296         catch( RuntimeException e )
297         {
298             throw e;
299         }
300         catch(Exception ee)
301         {
302             String msg = "Error getting iterator for #foreach at "+uberInfo;
303             rsvc.getLog().error(msg, ee);
304             throw new VelocityException(msg, ee);
305         }
306 
307         if (i == null)
308         {
309             if (skipInvalidIterator)
310             {
311                 return false;
312             }
313             else
314             {
315                 Node pnode = node.jjtGetChild(2);
316                 String msg = "#foreach parameter " + pnode.literal() + " at "
317                     + Log.formatFileString(pnode)
318                     + " is of type " + listObject.getClass().getName()
319                     + " and is either of wrong type or cannot be iterated.";
320                 rsvc.getLog().error(msg);
321                 throw new VelocityException(msg);
322             }
323         }
324 
325         int counter = counterInitialValue;
326         boolean maxNbrLoopsExceeded = false;
327 
328         /*
329          *  save the element key if there is one, and the loop counter
330          */
331         Object o = context.get(elementKey);
332         Object savedCounter = context.get(counterName);
333         Object nextFlag = context.get(hasNextName);
334         
335         /*
336          * Instantiate the null holder context if a null value
337          * is returned by the foreach iterator.  Only one instance is
338          * created - it's reused for every null value.
339          */
340         NullHolderContext nullHolderContext = null;
341 
342         while (!maxNbrLoopsExceeded && i.hasNext())
343         {
344             // TODO: JDK 1.5+ -> Integer.valueOf()
345             put(context, counterName , new Integer(counter));
346             Object value = i.next();
347             put(context, hasNextName, Boolean.valueOf(i.hasNext()));
348             put(context, elementKey, value);
349 
350             try
351             {
352                 /*
353                  * If the value is null, use the special null holder context
354                  */
355                 if (value == null)
356                 {
357                     if (nullHolderContext == null)
358                     {
359                         // lazy instantiation
360                         nullHolderContext = new NullHolderContext(elementKey, context);
361                     }
362                     node.jjtGetChild(3).render(nullHolderContext, writer);
363                 }
364                 else
365                 {
366                     node.jjtGetChild(3).render(context, writer);
367                 }
368             }
369             catch (Break.BreakException ex)
370             {
371                 // encountered #break directive inside #foreach loop
372                 break;
373             }
374             
375             counter++;
376 
377             // Determine whether we're allowed to continue looping.
378             // ASSUMPTION: counterInitialValue is not negative!
379             maxNbrLoopsExceeded = (counter - counterInitialValue) >= maxNbrLoops;
380         }
381 
382         /*
383          * restores the loop counter (if we were nested)
384          * if we have one, else just removes
385          */
386 
387         if (savedCounter != null)
388         {
389             context.put(counterName, savedCounter);
390         }
391         else
392         {
393             context.remove(counterName);
394         }
395 
396 
397         /*
398          *  restores element key if exists
399          *  otherwise just removes
400          */
401 
402         if (o != null)
403         {
404             context.put(elementKey, o);
405         }
406         else
407         {
408             context.remove(elementKey);
409         }
410 
411         /*
412          * restores the "hasNext" boolean flag if it exists
413          */         
414         if( nextFlag != null )
415         {
416             context.put(hasNextName, nextFlag);
417         }
418         else
419         {
420             context.remove(hasNextName);
421         }
422 
423         return true;
424     }
425 }