    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    }