| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package org.apache.tapestry5.corelib.components; |
| |
| import org.apache.tapestry5.*; |
| import org.apache.tapestry5.annotations.*; |
| import org.apache.tapestry5.corelib.LoopFormState; |
| import org.apache.tapestry5.ioc.annotations.Inject; |
| import org.apache.tapestry5.ioc.internal.util.CollectionFactory; |
| import org.apache.tapestry5.services.ComponentDefaultProvider; |
| import org.apache.tapestry5.services.FormSupport; |
| import org.apache.tapestry5.services.Heartbeat; |
| |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** |
| * A basic looping component; loops over a number of items (provided by its source parameter), rendering its body for each |
| * one. When a Loop is inside a {@link Form}, it records quite a bit of state into the Form to coordinate access |
| * to the same (or equivalent) objects during the form submission as during the render. This is controlled by |
| * the formState parameter (of type {@link LoopFormState}) and can be 'none' (nothing stored into the form), 'values' |
| * (which stores the individual values looped over, or via a {@link ValueEncoder}, just the value's ids), and |
| * 'iteration' (which just stores indexes to the values within the source parameter, which means that the source |
| * parameter will be accessed during the form submission). |
| * |
| * For a non-volatile Loop inside the form, the Loop stores a series of commands that start and end |
| * {@linkplain Heartbeat heartbeats}, and stores state for each value in the source parameter (either as full objects |
| * when the encoder parameter is not bound, or as client-side objects when there is an encoder). For a Loop that doesn't |
| * need to be aware of the enclosing Form (if any), the formState parameter should be bound to 'none'. |
| * |
| * When the Loop is used inside a Form, it will generate an |
| * {@link org.apache.tapestry5.EventConstants#SYNCHRONIZE_VALUES} event to inform its container what values were |
| * submitted and in what order; this can allow the container to pre-load the values in a single batch form external |
| * storage, if that is appropriate. |
| * |
| * @tapestrydoc |
| */ |
| @SupportsInformalParameters |
| @Events(EventConstants.SYNCHRONIZE_VALUES) |
| public class Loop<T> |
| { |
| /** |
| * Setup command for non-volatile rendering. |
| */ |
| private static final ComponentAction<Loop> RESET_INDEX = new ComponentAction<Loop>() |
| { |
| private static final long serialVersionUID = 6477493424977597345L; |
| |
| public void execute(Loop component) |
| { |
| component.resetIndex(); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return "Loop.ResetIndex"; |
| } |
| }; |
| |
| /** |
| * Setup command for volatile rendering. Volatile rendering relies on re-acquiring the Iterator and working our way |
| * through it (and hoping for the best!). |
| */ |
| private static final ComponentAction<Loop> SETUP_FOR_VOLATILE = new ComponentAction<Loop>() |
| { |
| private static final long serialVersionUID = -977168791667037377L; |
| |
| public void execute(Loop component) |
| { |
| component.setupForVolatile(); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return "Loop.SetupForVolatile"; |
| } |
| }; |
| |
| /** |
| * Advances to next value in a volatile way. So, the <em>number</em> of steps is intrinsically stored in the Form |
| * (as the number of ADVANCE_VOLATILE commands), but the actual values are expressly stored only on the server. |
| */ |
| private static final ComponentAction<Loop> ADVANCE_VOLATILE = new ComponentAction<Loop>() |
| { |
| private static final long serialVersionUID = -4600281573714776832L; |
| |
| public void execute(Loop component) |
| { |
| component.advanceVolatile(); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return "Loop.AdvanceVolatile"; |
| } |
| }; |
| |
| /** |
| * Used in both volatile and non-volatile mode to end the current heartbeat (started by either ADVANCE_VOLATILE or |
| * one of the RestoreState commands). Also increments the index. |
| */ |
| private static final ComponentAction<Loop> END_HEARTBEAT = new ComponentAction<Loop>() |
| { |
| private static final long serialVersionUID = -977168791667037377L; |
| |
| public void execute(Loop component) |
| { |
| component.endHeartbeat(); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return "Loop.EndHeartbeat"; |
| } |
| }; |
| |
| /** |
| * Restores a state value (this is the case when there is no encoder and the complete value is stored). |
| */ |
| static class RestoreState implements ComponentAction<Loop> |
| { |
| private static final long serialVersionUID = -3926831611368720764L; |
| |
| private final Object storedValue; |
| |
| public RestoreState(final Object storedValue) |
| { |
| this.storedValue = storedValue; |
| } |
| |
| public void execute(Loop component) |
| { |
| component.restoreState(storedValue); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return String.format("Loop.RestoreState[%s]", storedValue); |
| } |
| } |
| |
| /** |
| * Restores the value using a stored primary key via {@link ValueEncoder#toValue(String)}. |
| */ |
| static class RestoreStateFromStoredClientValue implements ComponentAction<Loop> |
| { |
| private final String clientValue; |
| |
| public RestoreStateFromStoredClientValue(final String clientValue) |
| { |
| this.clientValue = clientValue; |
| } |
| |
| public void execute(Loop component) |
| { |
| component.restoreStateFromStoredClientValue(clientValue); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return String.format("Loop.RestoreStateFromStoredClientValue[%s]", clientValue); |
| } |
| } |
| |
| /** |
| * Start of processing event that allows the Loop to set up internal bookeeping, to track which values have come up |
| * in the form submission. |
| */ |
| static final ComponentAction<Loop> PREPARE_FOR_SUBMISSION = new ComponentAction<Loop>() |
| { |
| public void execute(Loop component) |
| { |
| component.prepareForSubmission(); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return "Loop.PrepareForSubmission"; |
| } |
| }; |
| |
| static final ComponentAction<Loop> NOTIFY_CONTAINER = new ComponentAction<Loop>() |
| { |
| public void execute(Loop component) |
| { |
| component.notifyContainer(); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return "Loop.NotifyContainer"; |
| } |
| }; |
| |
| /** |
| * Defines the collection of values for the loop to iterate over. If not specified, defaults to a property of the |
| * container whose name matches the Loop cmponent's id. |
| */ |
| @Parameter(required = true, principal = true, autoconnect = true) |
| private Iterable<T> source; |
| |
| /** |
| * A ValueEncoder used to convert server-side objects (provided by the |
| * "value" parameter) into unique client-side strings (typically IDs) and |
| * back. In general, when using a non-volatile Loop in a Form, you should |
| * either provide a ValueEncoder with the encoder parameter or use a "value" |
| * type for which Tapestry is configured to provide a ValueEncoder |
| * automatically. Otherwise Tapestry must fall back to using the plain |
| * index of each loop iteration, rather than the ValueEncoder-provided |
| * unique ID, for recording state into the form. |
| */ |
| @Parameter |
| private ValueEncoder<T> encoder; |
| |
| /** |
| * Controls what information, if any, is encoded into an enclosing Form. The default value |
| * is {@link org.apache.tapestry5.corelib.LoopFormState#VALUES}. This parameter |
| * is only used if the component is enclosed by a Form. |
| */ |
| @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL) |
| private LoopFormState formState = LoopFormState.VALUES; |
| |
| @Environmental(false) |
| private FormSupport formSupport; |
| |
| /** |
| * The element to render. If not null, then the loop will render the indicated element around its body (on each pass |
| * through the loop). The default is derived from the component template. |
| */ |
| @Parameter(defaultPrefix = BindingConstants.LITERAL) |
| private String element; |
| |
| /** |
| * The current value, set before the component renders its body. |
| */ |
| @Parameter(principal = true) |
| private T value; |
| |
| /** |
| * The index into the source items. |
| */ |
| @Parameter |
| private int index; |
| |
| /** |
| * A Block to render instead of the loop when the source is empty. The default is to render nothing. |
| */ |
| @Parameter(defaultPrefix = BindingConstants.LITERAL) |
| private Block empty; |
| |
| private Iterator<T> iterator; |
| |
| @Environmental |
| private Heartbeat heartbeat; |
| |
| private boolean storeValuesInForm, storeIncrementsInForm, storeHeartbeatsInForm; |
| |
| @Inject |
| private ComponentResources resources; |
| |
| @Inject |
| private ComponentDefaultProvider defaultProvider; |
| |
| private Block cleanupBlock; |
| |
| /** |
| * Objects that have been recovered via {@link org.apache.tapestry5.ValueEncoder#toValue(String)} during the |
| * processing of the loop. These are sent to the container via an event. |
| */ |
| private List<T> synchonizedValues; |
| |
| LoopFormState defaultFormState() |
| { |
| return LoopFormState.VALUES; |
| } |
| |
| String defaultElement() |
| { |
| return resources.getElementName(); |
| } |
| |
| ValueEncoder defaultEncoder() |
| { |
| return defaultProvider.defaultValueEncoder("value", resources); |
| } |
| |
| @SetupRender |
| boolean setup() |
| { |
| index = 0; |
| |
| iterator = source == null ? null : source.iterator(); |
| |
| boolean insideForm = formSupport != null; |
| |
| storeValuesInForm = insideForm && formState == LoopFormState.VALUES; |
| storeIncrementsInForm = insideForm && formState == LoopFormState.ITERATION; |
| |
| storeHeartbeatsInForm = insideForm && formState != LoopFormState.NONE; |
| |
| if (storeValuesInForm) |
| formSupport.store(this, PREPARE_FOR_SUBMISSION); |
| |
| // Only render the body if there is something to iterate over |
| |
| boolean hasContent = iterator != null && iterator.hasNext(); |
| |
| if (insideForm && hasContent) |
| { |
| if (storeValuesInForm) |
| formSupport.store(this, RESET_INDEX); |
| if (storeIncrementsInForm) |
| formSupport.store(this, SETUP_FOR_VOLATILE); |
| } |
| |
| cleanupBlock = hasContent ? null : empty; |
| |
| // Jump directly to cleanupRender if there is no content |
| |
| return hasContent; |
| } |
| |
| /** |
| * Returns the empty block, or null, after the render has finished. It will only be the empty block (which itself |
| * may be null) if the source was null or empty. |
| */ |
| Block cleanupRender() |
| { |
| if (storeValuesInForm) |
| formSupport.store(this, NOTIFY_CONTAINER); |
| |
| return cleanupBlock; |
| } |
| |
| private void setupForVolatile() |
| { |
| index = 0; |
| iterator = source.iterator(); |
| } |
| |
| private void advanceVolatile() |
| { |
| value = iterator.next(); |
| |
| startHeartbeat(); |
| } |
| |
| /** |
| * Begins a new heartbeat. |
| */ |
| @BeginRender |
| void begin(MarkupWriter writer) |
| { |
| value = iterator.next(); |
| |
| if (storeValuesInForm) |
| { |
| if (encoder == null) |
| { |
| formSupport.store(this, new RestoreState(value)); |
| } |
| else |
| { |
| String clientValue = encoder.toClient(value); |
| |
| formSupport.store(this, new RestoreStateFromStoredClientValue(clientValue)); |
| } |
| } |
| |
| if (storeIncrementsInForm) |
| { |
| formSupport.store(this, ADVANCE_VOLATILE); |
| } |
| |
| startHeartbeat(); |
| |
| if (element != null) |
| { |
| writer.element(element); |
| resources.renderInformalParameters(writer); |
| } |
| } |
| |
| private void startHeartbeat() |
| { |
| heartbeat.begin(); |
| } |
| |
| /** |
| * Ends the current heartbeat. |
| */ |
| @AfterRender |
| Boolean after(MarkupWriter writer) |
| { |
| if (element != null) |
| writer.end(); |
| |
| endHeartbeat(); |
| |
| if (storeHeartbeatsInForm) |
| { |
| formSupport.store(this, END_HEARTBEAT); |
| } |
| |
| return iterator.hasNext() ? false : null; |
| } |
| |
| private void endHeartbeat() |
| { |
| heartbeat.end(); |
| |
| index++; |
| } |
| |
| private void resetIndex() |
| { |
| index = 0; |
| } |
| |
| /** |
| * Restores state previously stored by the Loop into a Form. |
| */ |
| private void restoreState(T storedValue) |
| { |
| value = storedValue; |
| |
| startHeartbeat(); |
| } |
| |
| /** |
| * Restores state previously encoded by the Loop and stored into the Form. |
| */ |
| private void restoreStateFromStoredClientValue(String clientValue) |
| { |
| // We assume that if an encoder is available when we rendered, that one will be available |
| // when the form is submitted. |
| |
| T restoredValue = encoder.toValue(clientValue); |
| |
| restoreState(restoredValue); |
| |
| synchonizedValues.add(restoredValue); |
| } |
| |
| private void prepareForSubmission() |
| { |
| synchonizedValues = CollectionFactory.newList(); |
| } |
| |
| private void notifyContainer() |
| { |
| Object[] values = synchonizedValues.toArray(); |
| |
| resources.triggerEvent(EventConstants.SYNCHRONIZE_VALUES, values, null); |
| } |
| |
| // For testing: |
| |
| public int getIndex() |
| { |
| return index; |
| } |
| |
| public T getValue() |
| { |
| return value; |
| } |
| |
| void setSource(Iterable<T> source) |
| { |
| this.source = source; |
| } |
| |
| void setHeartbeat(Heartbeat heartbeat) |
| { |
| this.heartbeat = heartbeat; |
| } |
| } |