blob: f03d0ed997df66a32256ddbd0ff5d823d98d9e5d [file] [log] [blame]
// 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;
}
}