blob: 61d1eee34dc0a93ed95156c87652ded005538271 [file] [log] [blame]
// Copyright 2006, 2007, 2008, 2009 The Apache Software Foundation
//
// 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.io.Serializable;
import java.util.Iterator;
import java.util.List;
/**
* Basic looping class; loops over a number of items (provided by its source parameter), rendering its body for each
* one. It turns out that gettting the component to <em>not</em> store its state in the Form is very tricky and, in
* fact, a series of commands for starting and ending heartbeats, and advancing through the iterator, are still stored.
* For a non-volatile Loop inside the form, the Loop stores a series of commands that start and end heartbeats and store
* state (either as full objects when there 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'.
* <p/>
* 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.
*/
@SupportsInformalParameters
@Events(EventConstants.SYNCHRONIZE_VALUES)
public class Loop
{
/**
* 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 PrimaryKeyEncoder#toValue(Serializable)}.
*/
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<?> source;
/**
* Optional value converter; if provided (or defaulted) and inside a form and not volatile, then each iterated value
* is converted and stored into the form. A default for this is calculated from the type of the property bound to
* the value parameter.
*/
@Parameter
private ValueEncoder<Object> encoder;
/**
* If true and the Loop is enclosed by a Form, then the normal state saving logic is turned off. Defaults to false,
* enabling state saving logic within Forms. With the addition of the formState parameter, volatile simply sets a
* default for formState is formState is not specified.
*
* @deprecated in release 5.1.0.4, use the formState parameter instead.
*/
@Parameter(name = "volatile", principal = true)
@Deprecated
private boolean volatileState;
/**
* Controls what information, if any, is encoded into an enclosing Form. The default value for this is set by the
* deprecated volatile parameter. The normal default is {@link org.apache.tapestry5.corelib.LoopFormState#VALUES},
* but changes to {@link org.apache.tapestry5.corelib.LoopFormState#ITERATION} if volatile is true. This parameter
* is only used if the component is enclosed by a Form.
*/
@Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
private LoopFormState formState;
@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 Object 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<?> 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<Object> synchonizedValues;
LoopFormState defaultFormState()
{
return volatileState ? LoopFormState.ITERATION : 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();
}
private void endHeartbeat()
{
heartbeat.end();
index++;
}
private void resetIndex()
{
index = 0;
}
/**
* Restores state previously stored by the Loop into a Form.
*/
private void restoreState(Object 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.
Object 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:
int getIndex()
{
return index;
}
Object getValue()
{
return value;
}
void setSource(Iterable<?> source)
{
this.source = source;
}
void setHeartbeat(Heartbeat heartbeat)
{
this.heartbeat = heartbeat;
}
}