001 // Copyright 2006, 2007, 2008, 2009, 2011 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014
015 package org.apache.tapestry5.corelib.components;
016
017 import java.util.Iterator;
018 import java.util.List;
019
020 import org.apache.tapestry5.BindingConstants;
021 import org.apache.tapestry5.Block;
022 import org.apache.tapestry5.ComponentAction;
023 import org.apache.tapestry5.ComponentResources;
024 import org.apache.tapestry5.EventConstants;
025 import org.apache.tapestry5.MarkupWriter;
026 import org.apache.tapestry5.ValueEncoder;
027 import org.apache.tapestry5.annotations.AfterRender;
028 import org.apache.tapestry5.annotations.BeginRender;
029 import org.apache.tapestry5.annotations.Environmental;
030 import org.apache.tapestry5.annotations.Events;
031 import org.apache.tapestry5.annotations.Parameter;
032 import org.apache.tapestry5.annotations.SetupRender;
033 import org.apache.tapestry5.annotations.SupportsInformalParameters;
034 import org.apache.tapestry5.corelib.LoopFormState;
035 import org.apache.tapestry5.ioc.annotations.Inject;
036 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
037 import org.apache.tapestry5.services.ComponentDefaultProvider;
038 import org.apache.tapestry5.services.FormSupport;
039 import org.apache.tapestry5.services.Heartbeat;
040
041 /**
042 * A basic looping component; loops over a number of items (provided by its source parameter), rendering its body for each
043 * one. When a Loop is inside a {@link Form}, it records quite a bit of state into the Form to coordinate access
044 * to the same (or equivalent) objects during the form submission as during the render. This is controlled by
045 * the formState parameter (of type {@link LoopFormState}) and can be 'none' (nothing stored into the form), 'values'
046 * (which stores the individual values looped over, or via a {@link ValueEncoder}, just the value's ids), and
047 * 'iteration' (which just stores indexes to the values within the source parameter, which means that the source
048 * parameter will be accessed during the form submission).
049 * <p>
050 * For a non-volatile Loop inside the form, the Loop stores a series of commands that start and end
051 * {@linkplain Heartbeat heartbeats}, and stores state for each value in the source parameter (either as full objects
052 * when the encoder parameter is not bound, or as client-side objects when there is an encoder). For a Loop that doesn't
053 * need to be aware of the enclosing Form (if any), the formState parameter should be bound to 'none'.
054 * <p/>
055 * When the Loop is used inside a Form, it will generate an
056 * {@link org.apache.tapestry5.EventConstants#SYNCHRONIZE_VALUES} event to inform its container what values were
057 * submitted and in what order; this can allow the container to pre-load the values in a single batch form external
058 * storage, if that is appropriate.
059 *
060 * @tapestrydoc
061 */
062 @SupportsInformalParameters
063 @Events(EventConstants.SYNCHRONIZE_VALUES)
064 public class Loop<T>
065 {
066 /**
067 * Setup command for non-volatile rendering.
068 */
069 private static final ComponentAction<Loop> RESET_INDEX = new ComponentAction<Loop>()
070 {
071 private static final long serialVersionUID = 6477493424977597345L;
072
073 public void execute(Loop component)
074 {
075 component.resetIndex();
076 }
077
078 @Override
079 public String toString()
080 {
081 return "Loop.ResetIndex";
082 }
083 };
084
085 /**
086 * Setup command for volatile rendering. Volatile rendering relies on re-acquiring the Iterator and working our way
087 * through it (and hoping for the best!).
088 */
089 private static final ComponentAction<Loop> SETUP_FOR_VOLATILE = new ComponentAction<Loop>()
090 {
091 private static final long serialVersionUID = -977168791667037377L;
092
093 public void execute(Loop component)
094 {
095 component.setupForVolatile();
096 }
097
098 @Override
099 public String toString()
100 {
101 return "Loop.SetupForVolatile";
102 }
103 };
104
105 /**
106 * Advances to next value in a volatile way. So, the <em>number</em> of steps is intrinsically stored in the Form
107 * (as the number of ADVANCE_VOLATILE commands), but the actual values are expressly stored only on the server.
108 */
109 private static final ComponentAction<Loop> ADVANCE_VOLATILE = new ComponentAction<Loop>()
110 {
111 private static final long serialVersionUID = -4600281573714776832L;
112
113 public void execute(Loop component)
114 {
115 component.advanceVolatile();
116 }
117
118 @Override
119 public String toString()
120 {
121 return "Loop.AdvanceVolatile";
122 }
123 };
124
125 /**
126 * Used in both volatile and non-volatile mode to end the current heartbeat (started by either ADVANCE_VOLATILE or
127 * one of the RestoreState commands). Also increments the index.
128 */
129 private static final ComponentAction<Loop> END_HEARTBEAT = new ComponentAction<Loop>()
130 {
131 private static final long serialVersionUID = -977168791667037377L;
132
133 public void execute(Loop component)
134 {
135 component.endHeartbeat();
136 }
137
138 @Override
139 public String toString()
140 {
141 return "Loop.EndHeartbeat";
142 }
143 };
144
145 /**
146 * Restores a state value (this is the case when there is no encoder and the complete value is stored).
147 */
148 static class RestoreState implements ComponentAction<Loop>
149 {
150 private static final long serialVersionUID = -3926831611368720764L;
151
152 private final Object storedValue;
153
154 public RestoreState(final Object storedValue)
155 {
156 this.storedValue = storedValue;
157 }
158
159 public void execute(Loop component)
160 {
161 component.restoreState(storedValue);
162 }
163
164 @Override
165 public String toString()
166 {
167 return String.format("Loop.RestoreState[%s]", storedValue);
168 }
169 }
170
171 /**
172 * Restores the value using a stored primary key via {@link ValueEncoder#toValue(String)}.
173 */
174 static class RestoreStateFromStoredClientValue implements ComponentAction<Loop>
175 {
176 private final String clientValue;
177
178 public RestoreStateFromStoredClientValue(final String clientValue)
179 {
180 this.clientValue = clientValue;
181 }
182
183 public void execute(Loop component)
184 {
185 component.restoreStateFromStoredClientValue(clientValue);
186 }
187
188 @Override
189 public String toString()
190 {
191 return String.format("Loop.RestoreStateFromStoredClientValue[%s]", clientValue);
192 }
193 }
194
195 /**
196 * Start of processing event that allows the Loop to set up internal bookeeping, to track which values have come up
197 * in the form submission.
198 */
199 static final ComponentAction<Loop> PREPARE_FOR_SUBMISSION = new ComponentAction<Loop>()
200 {
201 public void execute(Loop component)
202 {
203 component.prepareForSubmission();
204 }
205
206 @Override
207 public String toString()
208 {
209 return "Loop.PrepareForSubmission";
210 }
211 };
212
213 static final ComponentAction<Loop> NOTIFY_CONTAINER = new ComponentAction<Loop>()
214 {
215 public void execute(Loop component)
216 {
217 component.notifyContainer();
218 }
219
220 @Override
221 public String toString()
222 {
223 return "Loop.NotifyContainer";
224 }
225 };
226
227 /**
228 * Defines the collection of values for the loop to iterate over. If not specified, defaults to a property of the
229 * container whose name matches the Loop cmponent's id.
230 */
231 @Parameter(required = true, principal = true, autoconnect = true)
232 private Iterable<T> source;
233
234 /**
235 * A ValueEncoder used to convert server-side objects (provided by the
236 * "value" parameter) into unique client-side strings (typically IDs) and
237 * back. In general, when using a non-volatile Loop in a Form, you should
238 * either provide a ValueEncoder with the encoder parameter or use a "value"
239 * type for which Tapestry is configured to provide a ValueEncoder
240 * automatically. Otherwise Tapestry must fall back to using the plain
241 * index of each loop iteration, rather than the ValueEncoder-provided
242 * unique ID, for recording state into the form.
243 */
244 @Parameter
245 private ValueEncoder<T> encoder;
246
247 /**
248 * Controls what information, if any, is encoded into an enclosing Form. The default value
249 * is {@link org.apache.tapestry5.corelib.LoopFormState#VALUES}. This parameter
250 * is only used if the component is enclosed by a Form.
251 */
252 @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
253 private LoopFormState formState = LoopFormState.VALUES;
254
255 @Environmental(false)
256 private FormSupport formSupport;
257
258 /**
259 * The element to render. If not null, then the loop will render the indicated element around its body (on each pass
260 * through the loop). The default is derived from the component template.
261 */
262 @Parameter(defaultPrefix = BindingConstants.LITERAL)
263 private String element;
264
265 /**
266 * The current value, set before the component renders its body.
267 */
268 @Parameter(principal = true)
269 private T value;
270
271 /**
272 * The index into the source items.
273 */
274 @Parameter
275 private int index;
276
277 /**
278 * A Block to render instead of the loop when the source is empty. The default is to render nothing.
279 */
280 @Parameter(defaultPrefix = BindingConstants.LITERAL)
281 private Block empty;
282
283 private Iterator<T> iterator;
284
285 @Environmental
286 private Heartbeat heartbeat;
287
288 private boolean storeValuesInForm, storeIncrementsInForm, storeHeartbeatsInForm;
289
290 @Inject
291 private ComponentResources resources;
292
293 @Inject
294 private ComponentDefaultProvider defaultProvider;
295
296 private Block cleanupBlock;
297
298 /**
299 * Objects that have been recovered via {@link org.apache.tapestry5.ValueEncoder#toValue(String)} during the
300 * processing of the loop. These are sent to the container via an event.
301 */
302 private List<T> synchonizedValues;
303
304 LoopFormState defaultFormState()
305 {
306 return LoopFormState.VALUES;
307 }
308
309 String defaultElement()
310 {
311 return resources.getElementName();
312 }
313
314 ValueEncoder defaultEncoder()
315 {
316 return defaultProvider.defaultValueEncoder("value", resources);
317 }
318
319 @SetupRender
320 boolean setup()
321 {
322 index = 0;
323
324 iterator = source == null ? null : source.iterator();
325
326 boolean insideForm = formSupport != null;
327
328 storeValuesInForm = insideForm && formState == LoopFormState.VALUES;
329 storeIncrementsInForm = insideForm && formState == LoopFormState.ITERATION;
330
331 storeHeartbeatsInForm = insideForm && formState != LoopFormState.NONE;
332
333 if (storeValuesInForm)
334 formSupport.store(this, PREPARE_FOR_SUBMISSION);
335
336 // Only render the body if there is something to iterate over
337
338 boolean hasContent = iterator != null && iterator.hasNext();
339
340 if (insideForm && hasContent)
341 {
342 if (storeValuesInForm)
343 formSupport.store(this, RESET_INDEX);
344 if (storeIncrementsInForm)
345 formSupport.store(this, SETUP_FOR_VOLATILE);
346 }
347
348 cleanupBlock = hasContent ? null : empty;
349
350 // Jump directly to cleanupRender if there is no content
351
352 return hasContent;
353 }
354
355 /**
356 * Returns the empty block, or null, after the render has finished. It will only be the empty block (which itself
357 * may be null) if the source was null or empty.
358 */
359 Block cleanupRender()
360 {
361 if (storeValuesInForm)
362 formSupport.store(this, NOTIFY_CONTAINER);
363
364 return cleanupBlock;
365 }
366
367 private void setupForVolatile()
368 {
369 index = 0;
370 iterator = source.iterator();
371 }
372
373 private void advanceVolatile()
374 {
375 value = iterator.next();
376
377 startHeartbeat();
378 }
379
380 /**
381 * Begins a new heartbeat.
382 */
383 @BeginRender
384 void begin(MarkupWriter writer)
385 {
386 value = iterator.next();
387
388 if (storeValuesInForm)
389 {
390 if (encoder == null)
391 {
392 formSupport.store(this, new RestoreState(value));
393 }
394 else
395 {
396 String clientValue = encoder.toClient(value);
397
398 formSupport.store(this, new RestoreStateFromStoredClientValue(clientValue));
399 }
400 }
401
402 if (storeIncrementsInForm)
403 {
404 formSupport.store(this, ADVANCE_VOLATILE);
405 }
406
407 startHeartbeat();
408
409 if (element != null)
410 {
411 writer.element(element);
412 resources.renderInformalParameters(writer);
413 }
414 }
415
416 private void startHeartbeat()
417 {
418 heartbeat.begin();
419 }
420
421 /**
422 * Ends the current heartbeat.
423 */
424 @AfterRender
425 Boolean after(MarkupWriter writer)
426 {
427 if (element != null)
428 writer.end();
429
430 endHeartbeat();
431
432 if (storeHeartbeatsInForm)
433 {
434 formSupport.store(this, END_HEARTBEAT);
435 }
436
437 return iterator.hasNext() ? false : null;
438 }
439
440 private void endHeartbeat()
441 {
442 heartbeat.end();
443
444 index++;
445 }
446
447 private void resetIndex()
448 {
449 index = 0;
450 }
451
452 /**
453 * Restores state previously stored by the Loop into a Form.
454 */
455 private void restoreState(T storedValue)
456 {
457 value = storedValue;
458
459 startHeartbeat();
460 }
461
462 /**
463 * Restores state previously encoded by the Loop and stored into the Form.
464 */
465 private void restoreStateFromStoredClientValue(String clientValue)
466 {
467 // We assume that if an encoder is available when we rendered, that one will be available
468 // when the form is submitted.
469
470 T restoredValue = encoder.toValue(clientValue);
471
472 restoreState(restoredValue);
473
474 synchonizedValues.add(restoredValue);
475 }
476
477 private void prepareForSubmission()
478 {
479 synchonizedValues = CollectionFactory.newList();
480 }
481
482 private void notifyContainer()
483 {
484 Object[] values = synchonizedValues.toArray();
485
486 resources.triggerEvent(EventConstants.SYNCHRONIZE_VALUES, values, null);
487 }
488
489 // For testing:
490
491 public int getIndex()
492 {
493 return index;
494 }
495
496 public T getValue()
497 {
498 return value;
499 }
500
501 void setSource(Iterable<T> source)
502 {
503 this.source = source;
504 }
505
506 void setHeartbeat(Heartbeat heartbeat)
507 {
508 this.heartbeat = heartbeat;
509 }
510 }