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