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 945927 2010-05-18 22:21:41Z nbubna $
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     // track if we've done the deprecation warning thing already
188     private boolean warned = false;
189 
190     /**
191      *  immutable, so create in init
192      */
193     protected Info uberInfo;
194 
195     /**
196      *  simple init - init the tree and get the elementKey from
197      *  the AST
198      * @param rs
199      * @param context
200      * @param node
201      * @throws TemplateInitException
202      */
203     public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
204         throws TemplateInitException
205     {
206         super.init(rs, context, node);
207 
208         // handle deprecated config settings
209         counterName = rsvc.getString(RuntimeConstants.COUNTER_NAME);
210         hasNextName = rsvc.getString(RuntimeConstants.HAS_NEXT_NAME);
211         counterInitialValue = rsvc.getInt(RuntimeConstants.COUNTER_INITIAL_VALUE);
212         // only warn once per instance...
213         if (!warned && rsvc.getLog().isWarnEnabled())
214         {
215             warned = true;
216             // ...and only if they customize these settings
217             if (!"velocityCount".equals(counterName))
218             {
219                 rsvc.getLog().warn("The "+RuntimeConstants.COUNTER_NAME+
220                     " property has been deprecated. It will be removed"+
221                     " (along with $velocityCount itself) in Velocity 2.0. "+
222                     " Instead, please use $foreach.count to access"+
223                     " the loop counter.");
224             }
225             if (!"velocityHasNext".equals(hasNextName))
226             {
227                 rsvc.getLog().warn("The "+RuntimeConstants.HAS_NEXT_NAME+
228                     " property has been deprecated. It will be removed"+
229                     " (along with $velocityHasNext itself ) in Velocity 2.0. "+
230                     " Instead, please use $foreach.hasNext to access"+
231                     " this value from now on.");
232             }
233             if (counterInitialValue != 1)
234             {
235                 rsvc.getLog().warn("The "+RuntimeConstants.COUNTER_INITIAL_VALUE+
236                     " property has been deprecated. It will be removed"+
237                     " (along with $velocityCount itself) in Velocity 2.0. "+
238                     " Instead, please use $foreach.index to access"+
239                     " the 0-based loop index and $foreach.count"+
240                     " to access the 1-based loop counter.");
241             }
242         }
243 
244         maxNbrLoops = rsvc.getInt(RuntimeConstants.MAX_NUMBER_LOOPS,
245                                   Integer.MAX_VALUE);
246         if (maxNbrLoops < 1)
247         {
248             maxNbrLoops = Integer.MAX_VALUE;
249         }
250         skipInvalidIterator =
251             rsvc.getBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, true);
252         
253         if (rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false))
254         {
255           // If we are in strict mode then the default for skipInvalidItarator
256           // is true.  However, if the property is explicitly set, then honor the setting.
257           skipInvalidIterator = rsvc.getBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, false);
258         }
259                 
260         /*
261          *  this is really the only thing we can do here as everything
262          *  else is context sensitive
263          */
264         SimpleNode sn = (SimpleNode) node.jjtGetChild(0);
265 
266         if (sn instanceof ASTReference)
267         {
268             elementKey = ((ASTReference) sn).getRootString();
269         }
270         else
271         {
272             /*
273              * the default, error-prone way which we'll remove
274              *  TODO : remove if all goes well
275              */
276             elementKey = sn.getFirstToken().image.substring(1);
277         }
278 
279         /*
280          * make an uberinfo - saves new's later on
281          */
282 
283         uberInfo = new Info(this.getTemplateName(),
284                 getLine(),getColumn());
285     }
286 
287     /**
288      * Extension hook to allow subclasses to control whether loop vars
289      * are set locally or not. So, those in favor of VELOCITY-285, can
290      * make that happen easily by overriding this and having it use
291      * context.localPut(k,v). See VELOCITY-630 for more on this.
292      */
293     protected void put(InternalContextAdapter context, String key, Object value)
294     {
295         context.put(key, value);
296     }
297 
298     /**
299      *  renders the #foreach() block
300      * @param context
301      * @param writer
302      * @param node
303      * @return True if the directive rendered successfully.
304      * @throws IOException
305      * @throws MethodInvocationException
306      * @throws ResourceNotFoundException
307      * @throws ParseErrorException
308      */
309     public boolean render(InternalContextAdapter context,
310                            Writer writer, Node node)
311         throws IOException,  MethodInvocationException, ResourceNotFoundException,
312         	ParseErrorException
313     {
314         /*
315          *  do our introspection to see what our collection is
316          */
317 
318         Object listObject = node.jjtGetChild(2).value(context);
319 
320         if (listObject == null)
321              return false;
322 
323         Iterator i = null;
324 
325         try
326         {
327             i = rsvc.getUberspect().getIterator(listObject, uberInfo);
328         }
329         /**
330          * pass through application level runtime exceptions
331          */
332         catch( RuntimeException e )
333         {
334             throw e;
335         }
336         catch(Exception ee)
337         {
338             String msg = "Error getting iterator for #foreach at "+uberInfo;
339             rsvc.getLog().error(msg, ee);
340             throw new VelocityException(msg, ee);
341         }
342 
343         if (i == null)
344         {
345             if (skipInvalidIterator)
346             {
347                 return false;
348             }
349             else
350             {
351                 Node pnode = node.jjtGetChild(2);
352                 String msg = "#foreach parameter " + pnode.literal() + " at "
353                     + Log.formatFileString(pnode)
354                     + " is of type " + listObject.getClass().getName()
355                     + " and is either of wrong type or cannot be iterated.";
356                 rsvc.getLog().error(msg);
357                 throw new VelocityException(msg);
358             }
359         }
360 
361         int counter = counterInitialValue;
362         boolean maxNbrLoopsExceeded = false;
363 
364         /*
365          *  save the element key if there is one, and the loop counter
366          */
367         Object o = context.get(elementKey);
368         Object savedCounter = context.get(counterName);
369         Object nextFlag = context.get(hasNextName);
370 
371         /*
372          * roll our own scope class instead of using preRender(ctx)'s
373          */
374         ForeachScope foreach = null;
375         if (isScopeProvided())
376         {
377             String name = getScopeName();
378             foreach = new ForeachScope(this, context.get(name));
379             context.put(name, foreach);
380         }
381 
382         /*
383          * Instantiate the null holder context if a null value
384          * is returned by the foreach iterator.  Only one instance is
385          * created - it's reused for every null value.
386          */
387         NullHolderContext nullHolderContext = null;
388 
389         while (!maxNbrLoopsExceeded && i.hasNext())
390         {
391             // TODO: JDK 1.5+ -> Integer.valueOf()
392             put(context, counterName , new Integer(counter));
393             Object value = i.next();
394             put(context, hasNextName, Boolean.valueOf(i.hasNext()));
395             put(context, elementKey, value);
396 
397             if (isScopeProvided())
398             {
399                 // update the scope control
400                 foreach.index++;
401                 foreach.hasNext = i.hasNext();
402             }
403 
404             try
405             {
406                 /*
407                  * If the value is null, use the special null holder context
408                  */
409                 if (value == null)
410                 {
411                     if (nullHolderContext == null)
412                     {
413                         // lazy instantiation
414                         nullHolderContext = new NullHolderContext(elementKey, context);
415                     }
416                     node.jjtGetChild(3).render(nullHolderContext, writer);
417                 }
418                 else
419                 {
420                     node.jjtGetChild(3).render(context, writer);
421                 }
422             }
423             catch (StopCommand stop)
424             {
425                 if (stop.isFor(this))
426                 {
427                     break;
428                 }
429                 else
430                 {
431                     // clean up first
432                     clean(context, o, savedCounter, nextFlag);
433                     throw stop;
434                 }
435             }
436             
437             counter++;
438 
439             // Determine whether we're allowed to continue looping.
440             // ASSUMPTION: counterInitialValue is not negative!
441             maxNbrLoopsExceeded = (counter - counterInitialValue) >= maxNbrLoops;
442         }
443         clean(context, o, savedCounter, nextFlag);
444         return true;
445     }
446 
447     protected void clean(InternalContextAdapter context,
448                          Object o, Object savedCounter, Object nextFlag)
449     {
450         /*
451          *  restores element key if exists
452          *  otherwise just removes
453          */
454         if (o != null)
455         {
456             context.put(elementKey, o);
457         }
458         else
459         {
460             context.remove(elementKey);
461         }
462 
463         /*
464          * restores the loop counter (if we were nested)
465          * if we have one, else just removes
466          */
467         if (savedCounter != null)
468         {
469             context.put(counterName, savedCounter);
470         }
471         else
472         {
473             context.remove(counterName);
474         }
475 
476         /*
477          * restores the "hasNext" boolean flag if it exists
478          */         
479         if (nextFlag != null)
480         {
481             context.put(hasNextName, nextFlag);
482         }
483         else
484         {
485             context.remove(hasNextName);
486         }
487 
488         // clean up after the ForeachScope
489         postRender(context);
490     }
491 }