    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.commons.util.CollectionFactory;
    018import org.apache.tapestry5.corelib.LoopFormState;
    019import org.apache.tapestry5.ioc.annotations.Inject;
    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}