    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}