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 }