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