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 }