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
015package org.apache.tapestry5.corelib.components;
016
017import java.util.Iterator;
018import java.util.List;
019
020import org.apache.tapestry5.BindingConstants;
021import org.apache.tapestry5.Block;
022import org.apache.tapestry5.ComponentAction;
023import org.apache.tapestry5.ComponentResources;
024import org.apache.tapestry5.EventConstants;
025import org.apache.tapestry5.MarkupWriter;
026import org.apache.tapestry5.ValueEncoder;
027import org.apache.tapestry5.annotations.AfterRender;
028import org.apache.tapestry5.annotations.BeginRender;
029import org.apache.tapestry5.annotations.Environmental;
030import org.apache.tapestry5.annotations.Events;
031import org.apache.tapestry5.annotations.Parameter;
032import org.apache.tapestry5.annotations.SetupRender;
033import org.apache.tapestry5.annotations.SupportsInformalParameters;
034import org.apache.tapestry5.corelib.LoopFormState;
035import org.apache.tapestry5.ioc.annotations.Inject;
036import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
037import org.apache.tapestry5.services.ComponentDefaultProvider;
038import org.apache.tapestry5.services.FormSupport;
039import 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)
064public 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}