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