• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 org.apache.velocity.context.InternalContextAdapter;
23 import org.apache.velocity.exception.TemplateInitException;
24 import org.apache.velocity.exception.VelocityException;
25 import org.apache.velocity.runtime.RuntimeConstants;
26 import org.apache.velocity.runtime.RuntimeServices;
27 import org.apache.velocity.runtime.parser.ParseException;
28 import org.apache.velocity.runtime.parser.Token;
29 import org.apache.velocity.runtime.parser.node.*;
30 import org.apache.velocity.util.StringUtils;
31 import org.apache.velocity.util.introspection.Info;
32 
33 import java.io.Closeable;
34 import java.io.IOException;
35 import java.io.Writer;
36 import java.util.ArrayList;
37 import java.util.Iterator;
38 
39 /**
40  * Foreach directive used for moving through arrays,
41  * or objects that provide an Iterator.
42  *
43  * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
44  * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
45  * @author Daniel Rall
46  * @version $Id$
47  */
48 public class Foreach extends Directive
49 {
50     /**
51      * Return name of this directive.
52      * @return The name of this directive.
53      */
54     @Override
getName()55     public String getName()
56     {
57         return "foreach";
58     }
59 
60     /**
61      * Return type of this directive.
62      * @return The type of this directive.
63      */
64     @Override
getType()65     public int getType()
66     {
67         return BLOCK;
68     }
69 
70     /**
71      * The maximum number of times we're allowed to loop.
72      */
73     private int maxNbrLoops;
74 
75     /**
76      * Whether or not to throw an Exception if the iterator is null.
77      */
78     private boolean skipInvalidIterator;
79 
80     /**
81      * The reference name used to access each
82      * of the elements in the list object. It
83      * is the $item in the following:
84      *
85      * #foreach ($item in $list)
86      *
87      * This can be used class wide because
88      * it is immutable.
89      */
90     private String elementKey;
91 
92     /**
93      *  immutable, so create in init
94      */
95     protected Info uberInfo;
96 
97     /**
98      *  simple init - init the tree and get the elementKey from
99      *  the AST
100      * @param rs
101      * @param context
102      * @param node
103      * @throws TemplateInitException
104      */
105     @Override
init(RuntimeServices rs, InternalContextAdapter context, Node node)106     public void init(RuntimeServices rs, InternalContextAdapter context, Node node)
107         throws TemplateInitException
108     {
109         super.init(rs, context, node);
110 
111         maxNbrLoops = rsvc.getInt(RuntimeConstants.MAX_NUMBER_LOOPS,
112                                   Integer.MAX_VALUE);
113         if (maxNbrLoops < 1)
114         {
115             maxNbrLoops = Integer.MAX_VALUE;
116         }
117         skipInvalidIterator =
118             rsvc.getBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, true);
119 
120         if (rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false))
121         {
122           // If we are in strict mode then the default for skipInvalidItarator
123           // is true.  However, if the property is explicitly set, then honor the setting.
124           skipInvalidIterator = rsvc.getBoolean(RuntimeConstants.SKIP_INVALID_ITERATOR, false);
125         }
126 
127         /*
128          *  this is really the only thing we can do here as everything
129          *  else is context sensitive
130          */
131         SimpleNode sn = (SimpleNode) node.jjtGetChild(0);
132 
133         if (sn instanceof ASTReference)
134         {
135             elementKey = ((ASTReference) sn).getRootString();
136         }
137         else
138         {
139             /*
140              * the default, error-prone way which we'll remove
141              */
142         	elementKey = sn.getFirstTokenImage().substring(1);
143         }
144 
145         /*
146          * make an uberinfo - saves new's later on
147          */
148 
149         uberInfo = new Info(this.getTemplateName(),
150                 getLine(),getColumn());
151     }
152 
153     /**
154      * Extension hook to allow subclasses to control whether loop vars
155      * are set locally or not. So, those in favor of VELOCITY-285, can
156      * make that happen easily by overriding this and having it use
157      * context.localPut(k,v). See VELOCITY-630 for more on this.
158      * @param context
159      * @param key
160      * @param value
161      */
put(InternalContextAdapter context, String key, Object value)162     protected void put(InternalContextAdapter context, String key, Object value)
163     {
164         context.put(key, value);
165     }
166 
167     /**
168      * Retrieve the contextual iterator.
169      * @param iterable
170      * @param node
171      * @return iterator
172      */
getIterator(Object iterable, Node node)173     protected Iterator getIterator(Object iterable, Node node)
174     {
175         Iterator i = null;
176         /*
177          * do our introspection to see what our collection is
178          */
179         if (iterable != null)
180         {
181             try
182             {
183                 i = rsvc.getUberspect().getIterator(iterable, uberInfo);
184             }
185             /*
186              * pass through application level runtime exceptions
187              */
188             catch (RuntimeException e)
189             {
190                 throw e;
191             }
192             catch (Exception ee)
193             {
194                 String msg = "Error getting iterator for #foreach parameter "
195                     + node.literal() + " at " + StringUtils.formatFileString(node);
196                 log.error(msg, ee);
197                 throw new VelocityException(msg, ee, rsvc.getLogContext().getStackTrace());
198             }
199 
200             if (i == null && !skipInvalidIterator)
201             {
202                 String msg = "#foreach parameter " + node.literal() + " at "
203                     + StringUtils.formatFileString(node) + " is of type " + iterable.getClass().getName()
204                     + " and cannot be iterated by " + rsvc.getUberspect().getClass().getName();
205                 log.error(msg);
206                 throw new VelocityException(msg, null, rsvc.getLogContext().getStackTrace());
207             }
208         }
209         return i;
210     }
211 
212     /**
213      *  renders the #foreach() block
214      * @param context
215      * @param writer
216      * @param node
217      * @return True if the directive rendered successfully.
218      * @throws IOException
219      */
220     @Override
render(InternalContextAdapter context, Writer writer, Node node)221     public boolean render(InternalContextAdapter context, Writer writer, Node node)
222         throws IOException
223     {
224         // Get the block ast tree which is always the last child ...
225         Node block = node.jjtGetChild(node.jjtGetNumChildren()-1);
226 
227         // ... except if there is an #else clause
228         Node elseBlock = null;
229         Node previous = node.jjtGetChild(node.jjtGetNumChildren()-2);
230         if (previous instanceof ASTBlock)
231         {
232             elseBlock = block;
233             block = previous;
234         }
235 
236         Node iterableNode = node.jjtGetChild(2);
237         Object iterable = iterableNode.value(context);
238         Iterator i = getIterator(iterable, iterableNode);
239         if (i == null || !i.hasNext())
240         {
241             if (elseBlock != null)
242             {
243                 renderBlock(context, writer, elseBlock);
244             }
245             return false;
246         }
247 
248         /*
249          * save the element key if there is one
250          */
251         Object o = context.get(elementKey);
252 
253         /*
254          * roll our own scope class instead of using preRender(ctx)'s
255          */
256         ForeachScope foreach = null;
257         if (isScopeProvided())
258         {
259             String name = getScopeName();
260             foreach = new ForeachScope(this, context.get(name));
261             context.put(name, foreach);
262         }
263 
264         int count = 1;
265         while (count <= maxNbrLoops && i.hasNext())
266         {
267             count++;
268 
269             put(context, elementKey, i.next());
270             if (isScopeProvided())
271             {
272                 // update the scope control
273                 foreach.index++;
274                 foreach.hasNext = i.hasNext();
275             }
276 
277             try
278             {
279                 renderBlock(context, writer, block);
280             }
281             catch (StopCommand stop)
282             {
283                 if (stop.isFor(this))
284                 {
285                     break;
286                 }
287                 else
288                 {
289                     // clean up first
290                     clean(context, o);
291                     throw stop;
292                 }
293             }
294         }
295         clean(context, o);
296         /*
297          * closes the iterator if it implements the Closeable interface
298          */
299         if (i instanceof Closeable && i != iterable) /* except if the iterable is the iterator itself */
300         {
301             ((Closeable)i).close();
302         }
303         return true;
304     }
305 
renderBlock(InternalContextAdapter context, Writer writer, Node block)306     protected void renderBlock(InternalContextAdapter context, Writer writer, Node block)
307         throws IOException
308     {
309         block.render(context, writer);
310     }
311 
clean(InternalContextAdapter context, Object o)312     protected void clean(InternalContextAdapter context, Object o)
313     {
314         /*
315          *  restores element key if exists
316          *  otherwise just removes
317          */
318         if (o != null)
319         {
320             context.put(elementKey, o);
321         }
322         else
323         {
324             context.remove(elementKey);
325         }
326 
327         // clean up after the ForeachScope
328         postRender(context);
329     }
330 
331     /**
332      * We do not allow a word token in any other arg position except for the 2nd since
333      * we are looking for the pattern #foreach($foo in $bar).
334      */
335     @Override
checkArgs(ArrayList<Integer> argtypes, Token t, String templateName)336     public void checkArgs(ArrayList<Integer> argtypes, Token t, String templateName)
337       throws ParseException
338     {
339         if (argtypes.size() < 3)
340         {
341             throw new MacroParseException("Too few arguments to the #foreach directive",
342               templateName, t);
343         }
344         else if (argtypes.get(0) != ParserTreeConstants.JJTREFERENCE)
345         {
346             throw new MacroParseException("Expected argument 1 of #foreach to be a reference",
347                 templateName, t);
348         }
349         else if (argtypes.get(1) != ParserTreeConstants.JJTWORD)
350         {
351             throw new MacroParseException("Expected word 'in' at argument position 2 in #foreach",
352                 templateName, t);
353         }
354         else if (argtypes.get(2) == ParserTreeConstants.JJTWORD)
355         {
356             throw new MacroParseException("Argument 3 of #foreach is of the wrong type",
357                 templateName, t);
358         }
359     }
360 }
361