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    }