blob: d6fbefd9217d3df3c4c81cd7c797de7df1c5d9fc [file] [log] [blame]
// Copyright 2004, 2005 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.tapestry.components;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.hivemind.ApplicationRuntimeException;
import org.apache.hivemind.HiveMind;
import org.apache.tapestry.IBinding;
import org.apache.tapestry.IForm;
import org.apache.tapestry.IMarkupWriter;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.Tapestry;
import org.apache.tapestry.TapestryUtils;
import org.apache.tapestry.coerce.ValueConverter;
import org.apache.tapestry.engine.NullWriter;
import org.apache.tapestry.form.AbstractFormComponent;
import org.apache.tapestry.markup.NestedMarkupWriterImpl;
import org.apache.tapestry.services.ComponentRenderWorker;
import org.apache.tapestry.services.DataSqueezer;
import org.apache.tapestry.services.ExpressionEvaluator;
import org.apache.tapestry.services.ResponseBuilder;
/**
* @author mb
* @since 4.0
* @see org.apache.tapestry.components.IPrimaryKeyConverter
* @see org.apache.tapestry.util.DefaultPrimaryKeyConverter
*/
public abstract class ForBean extends AbstractFormComponent
{
// constants
/**
* Prefix on the hidden value stored into the field to indicate the the actual value is stored
* (this is used when there is no primary key converter). The remainder of the string is a
* {@link DataSqueezer squeezed} representation of the value.
*/
private static final char DESC_VALUE = 'V';
/**
* Prefix on the hidden value stored into the field that indicates the primary key of the
* iterated value is stored; the remainder of the string is a {@link DataSqueezer squeezed}
* representation of the primary key. The {@link IPrimaryKeyConverter converter} is used to
* obtain the value from this key.
*/
private static final char DESC_PRIMARY_KEY = 'P';
private final RepSource _completeRepSource = new CompleteRepSource();
private final RepSource _keyExpressionRepSource = new KeyExpressionRepSource();
// intermediate members
private Object _value;
private int _index;
private boolean _rendering;
private boolean _hasNext = false;
// parameters
public abstract boolean getRenderTag();
public abstract String getElement();
public abstract String getKeyExpression();
public abstract IPrimaryKeyConverter getConverter();
public abstract Object getDefaultValue();
public abstract boolean getMatch();
public abstract boolean getVolatile();
// injects
public abstract DataSqueezer getDataSqueezer();
public abstract ValueConverter getValueConverter();
public abstract ExpressionEvaluator getExpressionEvaluator();
public abstract ComponentRenderWorker getRenderWorker();
public abstract ResponseBuilder getResponseBuilder();
public boolean hasNext()
{
return _hasNext;
}
/**
* Gets the source binding and iterates through its values. For each, it updates the value
* binding and render's its wrapped elements.
*/
protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle)
{
// form may be null if component is not located in a form
IForm form = (IForm) cycle.getAttribute(TapestryUtils.FORM_ATTRIBUTE);
// If the cycle is rewinding, but not this particular form,
// then do nothing (don't even render the body).
boolean cycleRewinding = cycle.isRewinding();
if (cycleRewinding && form != null && !form.isRewinding())
return;
setForm(form);
// Get the data to be iterated upon. Store in form if needed.
Iterator dataSource = getData(cycle, form);
// Do not iterate if dataSource is null.
// The dataSource was either not convertable to Iterator, or was empty.
if (dataSource == null)
return;
if (!cycleRewinding && form != null && !NullWriter.class.isInstance(writer))
form.setFormFieldUpdating(true);
String element = HiveMind.isNonBlank(getElement()) ? getElement() : getTemplateTagName();
boolean render = !cycleRewinding && HiveMind.isNonBlank(element) && getRenderTag();
IMarkupWriter loopWriter = writer;
// Perform the iterations
try
{
_index = 0;
_rendering = true;
while (dataSource.hasNext())
{
_hasNext = true;
// Get current value
_value = dataSource.next();
// Update output component parameters
updateOutputParameters();
// Render component
// swap out writers if necessary
if (getResponseBuilder().isDynamic()
&& getResponseBuilder().contains(this)
&& !NestedMarkupWriterImpl.class.isInstance(writer)) {
loopWriter = getResponseBuilder().getWriter(getClientId(), ResponseBuilder.ELEMENT_TYPE);
}
if (render) {
loopWriter.begin(element);
renderInformalParameters(loopWriter, cycle);
renderIdAttribute(loopWriter, cycle);
}
renderBody(loopWriter, cycle);
if (render) {
loopWriter.end(element);
}
_index++;
_hasNext = dataSource.hasNext();
// TODO: Fragile / messy
// Cause unique client id to be generated as well as event connection
// works or other after render workers. (basically reproduce what happens
// inside of AbstractComponent.render() . Perhaps this means it's time for
// refactoring of this logic, like deferring rendering to an actual component
// that can have its proper render() method invoked multiple times.
getRenderWorker().renderComponent(cycle, this);
generateClientId();
// set loopWriter back to original as the client ids/ etc change on each loop
loopWriter = writer;
}
}
finally
{
_rendering = false;
_value = null;
}
}
/**
* Overriden so that RenderWorker doesn't get run as we've been invoking
* it manually already.
*/
protected void cleanupAfterRender(IRequestCycle cycle)
{
}
protected void generateClientId()
{
String id = getSpecifiedId();
if (id != null && getPage() != null && getPage().getRequestCycle() != null)
setClientId(getPage().getRequestCycle().getUniqueId(TapestryUtils.convertTapestryIdToNMToken(id)));
}
/**
* Returns the most recent value extracted from the source parameter.
*
*/
public final Object getValue()
{
if (!_rendering)
throw Tapestry.createRenderOnlyPropertyException(this, "value");
return _value;
}
/**
* The index number, within the {@link #getStoredData(IRequestCycle, String) }, of the the current value.
*
*/
public int getIndex()
{
if (!_rendering)
throw Tapestry.createRenderOnlyPropertyException(this, "index");
return _index;
}
public boolean isDisabled()
{
return false;
}
/**
* Updates the index and value output parameters if bound.
*/
protected void updateOutputParameters()
{
IBinding indexBinding = getBinding("index");
if (indexBinding != null)
indexBinding.setObject(new Integer(_index));
IBinding valueBinding = getBinding("value");
if (valueBinding != null)
valueBinding.setObject(_value);
}
/**
* Updates the primaryKeys parameter if bound.
*/
protected void updatePrimaryKeysParameter(String[] stringReps)
{
IBinding primaryKeysBinding = getBinding("primaryKeys");
if (primaryKeysBinding == null)
return;
DataSqueezer squeezer = getDataSqueezer();
int repsCount = stringReps.length;
List primaryKeys = new ArrayList(repsCount);
for (int i = 0; i < stringReps.length; i++)
{
String rep = stringReps[i];
if (rep.length() == 0 || rep.charAt(0) != DESC_PRIMARY_KEY)
continue;
Object primaryKey = squeezer.unsqueeze(rep.substring(1));
primaryKeys.add(primaryKey);
}
primaryKeysBinding.setObject(primaryKeys);
}
// Do nothing in those methods, but make the JVM happy
protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle)
{
}
protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle)
{
}
/**
* Returns a list with the values to be iterated upon. The list is obtained in different ways: -
* If the component is not located in a form or 'volatile' is set to true, then the simply the
* values passed to 'source' are returned (same as Foreach) - If the component is in a form, and
* the form is rewinding, the values stored in the form are returned -- rewind is then always
* the same as render. - If the component is in a form, and the form is being rendered, the
* values are stored in the form as Hidden fields.
*
* @param cycle
* The current request cycle
* @param form
* The form within which the component is located (if any)
* @return An iterator with the values to be cycled upon
*/
private Iterator getData(IRequestCycle cycle, IForm form)
{
if (form == null || getVolatile())
return evaluateSourceIterator();
String name = form.getElementId(this);
if (cycle.isRewinding())
return getStoredData(cycle, name);
return storeSourceData(form, name);
}
/**
* Returns a list of the values stored as Hidden fields in the form. A conversion is performed
* if the primary key of the value is stored.
*
* @param cycle
* The current request cycle
* @param name
* The name of the HTTP parameter whether the values
* @return an iterator with the values stored in the provided Hidden fields
*/
protected Iterator getStoredData(IRequestCycle cycle, String name)
{
String[] stringReps = cycle.getParameters(name);
if (stringReps == null)
return null;
updatePrimaryKeysParameter(stringReps);
return new ReadSourceDataIterator(stringReps);
}
/**
* Pulls data from successive strings (posted by client-side hidden fields); each string
* representation may be either a value or a primary key.
*/
private class ReadSourceDataIterator implements Iterator
{
private final Iterator _sourceIterator = evaluateSourceIterator();
private final Iterator _fullSourceIterator = evaluateFullSourceIterator();
private final String[] _stringReps;
private int _index = 0;
private final Map _repToValueMap = new HashMap();
ReadSourceDataIterator(String[] stringReps)
{
_stringReps = stringReps;
}
public boolean hasNext()
{
return _index < _stringReps.length;
}
public Object next()
{
String rep = _stringReps[_index++];
return getValueFromStringRep(_sourceIterator, _fullSourceIterator, _repToValueMap, rep);
}
public void remove()
{
throw new UnsupportedOperationException("remove()");
}
}
/**
* Stores the provided data in the form and then returns the data as an iterator. If the primary
* key of the value can be determined, then that primary key is saved instead.
*
* @param form
* The form where the data will be stored
* @param name
* The name under which the data will be stored
* @return an iterator with the bound values stored in the form
*/
protected Iterator storeSourceData(IForm form, String name)
{
return new StoreSourceDataIterator(form, name, evaluateSourceIterator());
}
/**
* Iterates over a set of values, using {@link ForBean#getStringRepFromValue(Object)} to obtain
* the correct client-side string representation, and working with the form to store each
* successive value into the form.
*/
private class StoreSourceDataIterator implements Iterator
{
private final IForm _form;
private final String _name;
private final Iterator _delegate;
StoreSourceDataIterator(IForm form, String name, Iterator delegate)
{
_form = form;
_name = name;
_delegate = delegate;
}
public boolean hasNext()
{
return _delegate.hasNext();
}
public Object next()
{
Object value = _delegate.next();
String rep = getStringRepFromValue(value);
_form.addHiddenValue(_name, rep);
return value;
}
public void remove()
{
throw new UnsupportedOperationException("remove()");
}
}
/**
* Returns the string representation of the value. The first letter of the string representation
* shows whether a value or a primary key is being described.
*
* @param value
* @return The string representation of the given value.
*/
protected String getStringRepFromValue(Object value)
{
String rep;
DataSqueezer squeezer = getDataSqueezer();
// try to extract the primary key from the value
Object pk = getPrimaryKeyFromValue(value);
try {
if (pk != null) {
// Primary key was extracted successfully.
rep = DESC_PRIMARY_KEY + squeezer.squeeze(pk);
} else {
// primary key could not be extracted. squeeze value.
rep = DESC_VALUE + squeezer.squeeze(value);
}
} catch (Exception e) {
throw new ApplicationRuntimeException(ComponentMessages.keySqueezeError(this, value, e), this, getLocation(), e);
}
return rep;
}
/**
* Returns the primary key of the given value. Uses the 'keyExpression' or the 'converter' (if
* either is provided).
*
* @param value
* The value from which the primary key should be extracted
* @return The primary key of the value, or null if such cannot be extracted.
*/
protected Object getPrimaryKeyFromValue(Object value)
{
if (value == null)
return null;
Object primaryKey = getKeyExpressionFromValue(value);
if (primaryKey == null)
primaryKey = getConverterFromValue(value);
return primaryKey;
}
/**
* Uses the 'keyExpression' parameter to determine the primary key of the given value.
*
* @param value
* The value from which the primary key should be extracted
* @return The primary key of the value as defined by 'keyExpression', or null if such cannot be
* extracted.
*/
protected Object getKeyExpressionFromValue(Object value)
{
String keyExpression = getKeyExpression();
if (keyExpression == null)
return null;
Object primaryKey = getExpressionEvaluator().read(value, keyExpression);
return primaryKey;
}
/**
* Uses the 'converter' parameter to determine the primary key of the given value.
*
* @param value
* The value from which the primary key should be extracted
* @return The primary key of the value as provided by the converter, or null if such cannot be
* extracted.
*/
protected Object getConverterFromValue(Object value)
{
IPrimaryKeyConverter converter = getConverter();
if (converter == null)
return null;
Object primaryKey = converter.getPrimaryKey(value);
return primaryKey;
}
/**
* Determines the value that corresponds to the given string representation. If the 'match'
* parameter is true, attempt to find a value in 'source' or 'fullSource' that generates the
* same string representation. Otherwise, create a new value from the string representation.
*
* @param rep
* the string representation for which a value should be returned
* @return the value that corresponds to the provided string representation
*/
protected Object getValueFromStringRep(Iterator sourceIterator, Iterator fullSourceIterator,
Map repToValueMap, String rep)
{
Object value = null;
DataSqueezer squeezer = getDataSqueezer();
// Check if the string rep is empty. If so, just return the default value.
if (rep == null || rep.length() == 0)
return getDefaultValue();
// If required, find a value with an equivalent string representation and return it
boolean match = getMatch();
if (match)
{
value = findValueWithStringRep( sourceIterator, fullSourceIterator, repToValueMap,
rep, _completeRepSource);
if (value != null)
return value;
}
// Matching of the string representation was not successful or was disabled.
// Use the standard approaches to obtain the value from the rep.
char desc = rep.charAt(0);
String squeezed = rep.substring(1);
switch (desc)
{
case DESC_VALUE:
// If the string rep is just the value itself, unsqueeze it
value = squeezer.unsqueeze(squeezed);
break;
case DESC_PRIMARY_KEY:
// Perform keyExpression match if not already attempted
if (!match && getKeyExpression() != null)
value = findValueWithStringRep(
sourceIterator,
fullSourceIterator,
repToValueMap,
rep,
_keyExpressionRepSource);
// If 'converter' is defined, try to perform conversion from primary key to value
if (value == null)
{
IPrimaryKeyConverter converter = getConverter();
if (converter != null)
{
Object pk = squeezer.unsqueeze(squeezed);
value = converter.getValue(pk);
}
}
break;
}
if (value == null)
value = getDefaultValue();
return value;
}
/**
* Attempt to find a value in 'source' or 'fullSource' that generates the provided string
* representation. Use the RepSource interface to determine what the string representation of a
* particular value is.
*
* @param rep
* the string representation for which a value should be returned
* @param repSource
* an interface providing the string representation of a given value
* @return the value in 'source' or 'fullSource' that corresponds to the provided string
* representation
*/
protected Object findValueWithStringRep(Iterator sourceIterator, Iterator fullSourceIterator,
Map repToValueMap, String rep, RepSource repSource)
{
Object value = repToValueMap.get(rep);
if (value != null)
return value;
value = findValueWithStringRepInIterator(sourceIterator, repToValueMap, rep, repSource);
if (value != null)
return value;
value = findValueWithStringRepInIterator(fullSourceIterator, repToValueMap, rep, repSource);
return value;
}
/**
* Attempt to find a value in the provided collection that generates the required string
* representation. Use the RepSource interface to determine what the string representation of a
* particular value is.
*
* @param rep
* the string representation for which a value should be returned
* @param repSource
* an interface providing the string representation of a given value
* @param it
* the iterator of the collection in which a value should be searched
* @return the value in the provided collection that corresponds to the required string
* representation
*/
protected Object findValueWithStringRepInIterator(Iterator it, Map repToValueMap, String rep,
RepSource repSource)
{
while (it.hasNext())
{
Object sourceValue = it.next();
if (sourceValue == null)
continue;
String sourceRep = repSource.getStringRep(sourceValue);
repToValueMap.put(sourceRep, sourceValue);
if (rep.equals(sourceRep))
return sourceValue;
}
return null;
}
/**
* Returns a new iterator of the values in 'source'.
*
* @return the 'source' iterator
*/
protected Iterator evaluateSourceIterator()
{
Iterator it = null;
Object source = null;
IBinding sourceBinding = getBinding("source");
if (sourceBinding != null)
source = sourceBinding.getObject();
if (source != null)
it = (Iterator) getValueConverter().coerceValue(source, Iterator.class);
if (it == null)
it = Collections.EMPTY_LIST.iterator();
return it;
}
/**
* Returns a new iterator of the values in 'fullSource'.
*
* @return the 'fullSource' iterator
*/
protected Iterator evaluateFullSourceIterator()
{
Iterator it = null;
Object fullSource = null;
IBinding fullSourceBinding = getBinding("fullSource");
if (fullSourceBinding != null)
fullSource = fullSourceBinding.getObject();
if (fullSource != null)
it = (Iterator) getValueConverter().coerceValue(fullSource, Iterator.class);
if (it == null)
it = Collections.EMPTY_LIST.iterator();
return it;
}
/**
* An interface that provides the string representation of a given value.
*/
protected interface RepSource
{
String getStringRep(Object value);
}
/**
* An implementation of RepSource that provides the string representation of the given value
* using all methods.
*/
protected class CompleteRepSource implements RepSource
{
public String getStringRep(Object value)
{
return getStringRepFromValue(value);
}
}
/**
* An implementation of RepSource that provides the string representation of the given value
* using just the 'keyExpression' parameter.
*/
protected class KeyExpressionRepSource implements RepSource
{
public String getStringRep(Object value)
{
Object pk = getKeyExpressionFromValue(value);
return DESC_PRIMARY_KEY + getDataSqueezer().squeeze(pk);
}
}
/**
* For component can not take focus.
*/
protected boolean getCanTakeFocus()
{
return false;
}
public String getDisplayName()
{
return null;
}
}