blob: 839b1c50c6c7869ac2d2cc23aa0798e52bb90b08 [file] [log] [blame]
// Copyright 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.form;
import org.apache.hivemind.ApplicationRuntimeException;
import org.apache.hivemind.HiveMind;
import org.apache.hivemind.Location;
import org.apache.hivemind.util.Defense;
import org.apache.tapestry.*;
import org.apache.tapestry.engine.ILink;
import org.apache.tapestry.event.BrowserEvent;
import org.apache.tapestry.javascript.JavascriptManager;
import org.apache.tapestry.json.JSONObject;
import org.apache.tapestry.services.DataSqueezer;
import org.apache.tapestry.services.ResponseBuilder;
import org.apache.tapestry.services.ServiceConstants;
import org.apache.tapestry.valid.IValidationDelegate;
import java.util.*;
/**
* Encapsulates most of the behavior of a Form component.
*
*/
public class FormSupportImpl implements FormSupport
{
/**
* Name of query parameter storing the ids alloocated while rendering the form, as a comma
* seperated list. This information is used when the form is submitted, to ensure that the
* rewind allocates the exact same sequence of ids.
*/
public static final String FORM_IDS = "formids";
/**
* Names of additional ids that were pre-reserved, as a comma-sepereated list. These are names
* beyond that standard set. Certain engine services include extra parameter values that must be
* accounted for, and page properties may be encoded as additional query parameters.
*/
public static final String RESERVED_FORM_IDS = "reservedids";
/**
* {@link DataSqueezer} squeezed list of {@link IRequestCycle} id allocation state as it was just before this
* form was rendered. Is used to ensure that all generated form ids are globally unique and consistent
* between requests.
*/
public static final String SEED_IDS = "seedids";
/**
* Indicates why the form was submitted: whether for normal ("submit"), refresh, or because the
* form was canceled.
*/
public static final String SUBMIT_MODE = "submitmode";
/**
* Attribute set to true when a field has been focused; used to prevent conflicting JavaScript
* for field focusing from being emitted.
*/
public static final String FIELD_FOCUS_ATTRIBUTE = "org.apache.tapestry.field-focused";
private static final Set _standardReservedIds;
static
{
Set set = new HashSet();
set.addAll(Arrays.asList(ServiceConstants.RESERVED_IDS));
set.add(FORM_IDS);
set.add(RESERVED_FORM_IDS);
set.add(SEED_IDS);
set.add(SUBMIT_MODE);
set.add(FormConstants.SUBMIT_NAME_PARAMETER);
_standardReservedIds = Collections.unmodifiableSet(set);
}
private static final Set _submitModes;
static
{
Set set = new HashSet();
set.add(FormConstants.SUBMIT_CANCEL);
set.add(FormConstants.SUBMIT_NORMAL);
set.add(FormConstants.SUBMIT_REFRESH);
_submitModes = Collections.unmodifiableSet(set);
}
protected final IRequestCycle _cycle;
/**
* Used when rewinding the form to figure to match allocated ids (allocated during the rewind)
* against expected ids (allocated in the previous request cycle, when the form was rendered).
*/
private int _allocatedIdIndex;
/**
* The list of allocated ids for form elements within this form. This list is constructed when a
* form renders, and is validated against when the form is rewound.
*/
private final List _allocatedIds = new ArrayList();
private String _encodingType;
private final List _deferredRunnables = new ArrayList();
/**
* Map keyed on extended component id, value is the pre-rendered markup for that component.
*/
private final Map _prerenderMap = new HashMap();
/**
* {@link Map}, keyed on {@link FormEventType}. Values are either a String (the function name
* of a single event handler), or a List of Strings (a sequence of event handler function
* names).
*/
private Map _events;
private final IForm _form;
private final List _hiddenValues = new ArrayList();
private final boolean _rewinding;
private final IMarkupWriter _writer;
private final IValidationDelegate _delegate;
private final PageRenderSupport _pageRenderSupport;
/**
* Client side validation is built up using a json object syntax structure
*/
private final JSONObject _profile;
/**
* Used to detect whether or not a form component has been updated and will require form sync on ajax requests
*/
private boolean _fieldUpdating;
private JavascriptManager _javascriptManager;
private String _idSeed;
public FormSupportImpl(IMarkupWriter writer, IRequestCycle cycle, IForm form)
{
this(writer, cycle, form, null);
}
public FormSupportImpl(IMarkupWriter writer, IRequestCycle cycle,
IForm form, JavascriptManager javascriptManager)
{
Defense.notNull(writer, "writer");
Defense.notNull(cycle, "cycle");
Defense.notNull(form, "form");
_writer = writer;
_cycle = cycle;
_form = form;
_delegate = form.getDelegate();
_rewinding = cycle.isRewound(form);
_allocatedIdIndex = 0;
_pageRenderSupport = TapestryUtils.getOptionalPageRenderSupport(cycle);
_profile = new JSONObject();
_javascriptManager = javascriptManager;
}
/**
* Alternate constructor used for testing only.
*
* @param cycle
* The current cycle.
*/
FormSupportImpl(IRequestCycle cycle)
{
_cycle = cycle;
_form = null;
_rewinding = false;
_writer = null;
_delegate = null;
_pageRenderSupport = null;
_profile = null;
}
/**
* {@inheritDoc}
*/
public IForm getForm()
{
return _form;
}
/**
* {@inheritDoc}
*/
public void addEventHandler(FormEventType type, String functionName)
{
if (_events == null)
_events = new HashMap();
List functionList = (List) _events.get(type);
// The value can either be a String, or a List of String. Since
// it is rare for there to be more than one event handling function,
// we start with just a String.
if (functionList == null)
{
functionList = new ArrayList();
_events.put(type, functionList);
}
functionList.add(functionName);
}
/**
* Adds hidden fields for parameters provided by the {@link ILink}. These parameters define the
* information needed to dispatch the request, plus state information. The names of these
* parameters must be reserved so that conflicts don't occur that could disrupt the request
* processing. For example, if the id 'page' is not reserved, then a conflict could occur with a
* component whose id is 'page'. A certain number of ids are always reserved, and we find any
* additional ids beyond that set.
*/
private void addHiddenFieldsForLinkParameters(ILink link)
{
String[] names = link.getParameterNames();
int count = Tapestry.size(names);
StringBuffer extraIds = new StringBuffer();
String sep = "";
boolean hasExtra = false;
for (int i = 0; i < count; i++)
{
String name = names[i];
// Reserve the name.
if (!_standardReservedIds.contains(name))
{
_cycle.getUniqueId(name);
extraIds.append(sep);
extraIds.append(name);
sep = ",";
hasExtra = true;
}
addHiddenFieldsForLinkParameter(link, name);
}
if (hasExtra)
addHiddenValue(RESERVED_FORM_IDS, extraIds.toString());
}
public void addHiddenValue(String name, String value)
{
_hiddenValues.add(new HiddenFieldData(name, value));
}
public void addHiddenValue(String name, String id, String value)
{
_hiddenValues.add(new HiddenFieldData(name, id, value));
}
/**
* Converts the allocateIds property into a string, a comma-separated list of ids. This is
* included as a hidden field in the form and is used to identify discrepencies when the form is
* submitted.
*/
private String buildAllocatedIdList()
{
StringBuffer buffer = new StringBuffer();
int count = _allocatedIds.size();
for (int i = 0; i < count; i++)
{
if (i > 0)
buffer.append(',');
buffer.append(_allocatedIds.get(i));
}
return buffer.toString();
}
private void emitEventHandlers(String formId)
{
if (_events == null || _events.isEmpty())
return;
StringBuffer buffer = new StringBuffer();
Iterator i = _events.entrySet().iterator();
while (i.hasNext())
{
Map.Entry entry = (Map.Entry) i.next();
FormEventType type = (FormEventType) entry.getKey();
Object value = entry.getValue();
buffer.append("Tapestry.");
buffer.append(type.getAddHandlerFunctionName());
buffer.append("('");
buffer.append(formId);
buffer.append("', function (event)\n{");
List l = (List) value;
int count = l.size();
for (int j = 0; j < count; j++)
{
String functionName = (String) l.get(j);
if (j > 0)
{
buffer.append(";");
}
buffer.append("\n ");
buffer.append(functionName);
// It's supposed to be function names, but some of Paul's validation code
// adds inline code to be executed instead.
if (!functionName.endsWith(")"))
{
buffer.append("()");
}
}
buffer.append(";\n});\n");
}
// TODO: If PRS is null ...
_pageRenderSupport.addInitializationScript(_form, buffer.toString());
}
/**
* Constructs a unique identifier (within the Form). The identifier consists of the component's
* id, with an index number added to ensure uniqueness.
* <p>
* Simply invokes
* {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}with the
* component's id.
*/
public String getElementId(IFormComponent component)
{
return getElementId(component, component.getSpecifiedId());
}
/**
* Constructs a unique identifier (within the Form). The identifier consists of the component's
* id, with an index number added to ensure uniqueness.
* <p>
* Simply invokes
* {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}with the
* component's id.
*/
public String getElementId(IFormComponent component, String baseId)
{
// $ is not a valid character in an XML/XHTML id, so convert it to an underscore.
String filteredId = TapestryUtils.convertTapestryIdToNMToken(baseId);
String result = _cycle.getUniqueId(filteredId);
if (_rewinding)
{
if (_allocatedIdIndex >= _allocatedIds.size())
{
throw new StaleLinkException(FormMessages.formTooManyIds(_form, _allocatedIds.size(), component), component);
}
String expected = (String) _allocatedIds.get(_allocatedIdIndex);
if (!result.equals(expected))
throw new StaleLinkException(FormMessages.formIdMismatch(
_form,
_allocatedIdIndex,
expected,
result,
component), component);
}
else
{
_allocatedIds.add(result);
}
_allocatedIdIndex++;
component.setName(result);
component.setClientId(result);
return result;
}
public String peekClientId(IFormComponent comp)
{
String id = comp.getSpecifiedId();
if (id == null)
return null;
if (wasPrerendered(comp))
return comp.getClientId();
return _cycle.peekUniqueId(id);
}
public boolean isRewinding()
{
return _rewinding;
}
/**
* Invoked when rewinding a form to re-initialize the _allocatedIds and _elementIdAllocator.
* Converts a string passed as a parameter (and containing a comma separated list of ids) back
* into the allocateIds property. In addition, return the state of the ID allocater back to
* where it was at the start of the render.
*
* @see #buildAllocatedIdList()
* @since 3.0
*/
private void reinitializeIdAllocatorForRewind()
{
_cycle.initializeIdState(_cycle.getParameter(SEED_IDS));
String allocatedFormIds = _cycle.getParameter(FORM_IDS);
String[] ids = TapestryUtils.split(allocatedFormIds);
for (int i = 0; i < ids.length; i++)
_allocatedIds.add(ids[i]);
// Now, reconstruct the initial state of the
// id allocator.
String extraReservedIds = _cycle.getParameter(RESERVED_FORM_IDS);
ids = TapestryUtils.split(extraReservedIds);
for (int i = 0; i < ids.length; i++)
{
_cycle.getUniqueId(ids[i]);
}
}
public void render(String method, IRender informalParametersRenderer, ILink link, String scheme, Integer port)
{
String formId = _form.getName();
_idSeed = _cycle.encodeIdState();
emitEventManagerInitialization(formId);
// Convert the link's query parameters into a series of
// hidden field values (that will be rendered later).
addHiddenFieldsForLinkParameters(link);
// Create a hidden field to store the submission mode, in case
// client-side JavaScript forces an update.
addHiddenValue(SUBMIT_MODE, null);
// And another for the name of the component that
// triggered the submit.
addHiddenValue(FormConstants.SUBMIT_NAME_PARAMETER, null);
IMarkupWriter nested = _writer.getNestedWriter();
_form.renderBody(nested, _cycle);
runDeferredRunnables();
int portI = (port == null) ? 0 : port.intValue();
writeTag(_writer, method, link.getURL(scheme, null, portI, null, false));
// For XHTML compatibility
_writer.attribute("id", formId);
if (_encodingType != null)
_writer.attribute("enctype", _encodingType);
// Write out event handlers collected during the rendering.
emitEventHandlers(formId);
informalParametersRenderer.render(_writer, _cycle);
// Finish the <form> tag
_writer.println();
writeHiddenFields();
// Close the nested writer, inserting its contents.
nested.close();
// Close the <form> tag.
_writer.end();
String fieldId = _delegate.getFocusField();
if (_pageRenderSupport == null)
return;
_pageRenderSupport.addInitializationScript(_form, "dojo.require(\"tapestry.form\");");
// If the form doesn't support focus, or the focus has already been set by a different form,
// then do nothing.
if (!_cycle.isFocusDisabled() && fieldId != null && _form.getFocus()
&& _cycle.getAttribute(FIELD_FOCUS_ATTRIBUTE) == null)
{
// needs to happen last to avoid dialog issues in ie - TAPESTRY-1705
_pageRenderSupport.addScriptAfterInitialization(_form, "tapestry.form.focusField('" + fieldId + "');");
_cycle.setAttribute(FIELD_FOCUS_ATTRIBUTE, Boolean.TRUE);
}
// register the validation profile with client side form manager
if (_form.isClientValidationEnabled())
{
IPage page = _form.getPage();
// only include dojo widget layer if it's not already been included
if (!page.hasWidgets())
{
if (_javascriptManager != null && _javascriptManager.getFirstWidgetAsset() != null)
{
_pageRenderSupport.addExternalScript(_form,
_javascriptManager.getFirstWidgetAsset().getResourceLocation());
}
}
_pageRenderSupport.addInitializationScript(_form, "tapestry.form.clearProfiles('"
+ formId + "'); tapestry.form.registerProfile('" + formId + "',"
+ _profile.toString() + ");");
}
}
/**
* Pre-renders the form, setting up some client-side form support. Returns the name of the
* client-side form event manager variable.
*
* @param formId
* The client id of the form.
*/
protected void emitEventManagerInitialization(String formId)
{
if (_pageRenderSupport == null)
return;
StringBuffer str = new StringBuffer("dojo.require(\"tapestry.form\");");
str.append("tapestry.form.registerForm(\"").append(formId).append("\"");
if (_form.isAsync())
{
str.append(", true");
if (_form.isJson())
{
str.append(", true");
}
}
str.append(");");
_pageRenderSupport.addInitializationScript(_form, str.toString());
}
public String rewind()
{
_form.getDelegate().clear();
String mode = _cycle.getParameter(SUBMIT_MODE);
// On a cancel, don't bother rendering the body or anything else at all.
if (FormConstants.SUBMIT_CANCEL.equals(mode))
return mode;
reinitializeIdAllocatorForRewind();
_form.renderBody(_writer, _cycle);
// New, handles cases where an eventlistener
// causes a form submission.
BrowserEvent event = new BrowserEvent(_cycle);
_form.getEventInvoker().invokeFormListeners(this, _cycle, event);
int expected = _allocatedIds.size();
// The other case, _allocatedIdIndex > expected, is
// checked for inside getElementId(). Remember that
// _allocatedIdIndex is incremented after allocating.
if (_allocatedIdIndex < expected)
{
String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex);
throw new StaleLinkException(FormMessages.formTooFewIds(_form, expected - _allocatedIdIndex, nextExpectedId), _form);
}
runDeferredRunnables();
if (_submitModes.contains(mode))
{
// clear errors during refresh
if (FormConstants.SUBMIT_REFRESH.equals(mode))
{
_form.getDelegate().clearErrors();
}
return mode;
}
// Either something wacky on the client side, or a client without
// javascript enabled.
return FormConstants.SUBMIT_NORMAL;
}
private void runDeferredRunnables()
{
Iterator i = _deferredRunnables.iterator();
while (i.hasNext())
{
Runnable r = (Runnable) i.next();
r.run();
}
}
public void setEncodingType(String encodingType)
{
if (_encodingType != null && !_encodingType.equals(encodingType))
throw new ApplicationRuntimeException(FormMessages.encodingTypeContention(_form, _encodingType, encodingType),
_form, null, null);
_encodingType = encodingType;
}
/**
* Overwridden by {@link org.apache.tapestry.wml.GoFormSupportImpl} (WML).
*/
protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value)
{
writer.beginEmpty("input");
writer.attribute("type", "hidden");
writer.attribute("name", name);
if (HiveMind.isNonBlank(id))
writer.attribute("id", id);
writer.attribute("value", value == null ? "" : value);
writer.println();
}
/**
* Writes out all hidden values previously added by
* {@link #addHiddenValue(String, String, String)}. Writes a &lt;div&gt; tag around
* {@link #writeHiddenFieldList(IMarkupWriter)}. Overriden by
* {@link org.apache.tapestry.wml.GoFormSupportImpl}.
*/
protected void writeHiddenFields()
{
IMarkupWriter writer = getHiddenFieldWriter();
writer.begin("div");
writer.attribute("style", "display:none;");
writer.attribute("id", _form.getName() + "hidden");
writeHiddenFieldList(writer);
writer.end();
}
/**
* Writes out all hidden values previously added by
* {@link #addHiddenValue(String, String, String)}, plus the allocated id list.
*/
protected void writeHiddenFieldList(IMarkupWriter writer)
{
writeHiddenField(writer, FORM_IDS, null, buildAllocatedIdList());
writeHiddenField(writer, SEED_IDS, null, _idSeed);
Iterator i = _hiddenValues.iterator();
while (i.hasNext())
{
HiddenFieldData data = (HiddenFieldData) i.next();
writeHiddenField(writer, data.getName(), data.getId(), data.getValue());
}
}
/**
* Determines if a hidden field change has occurred, which would require
* that we write hidden form fields using the {@link ResponseBuilder}
* writer.
*
* @return The default {@link IMarkupWriter} if not doing a managed ajax/json
* response, else whatever is returned from {@link ResponseBuilder}.
*/
protected IMarkupWriter getHiddenFieldWriter()
{
if (_cycle.getResponseBuilder().contains(_form)
|| (!_fieldUpdating || !_cycle.getResponseBuilder().isDynamic()) )
{
return _writer;
}
return _cycle.getResponseBuilder().getWriter(_form.getName() + "hidden", ResponseBuilder.ELEMENT_TYPE);
}
private void addHiddenFieldsForLinkParameter(ILink link, String parameterName)
{
String[] values = link.getParameterValues(parameterName);
// In some cases, there are no values, but a space is "reserved" for the provided name.
if (values == null)
return;
for (int i = 0; i < values.length; i++)
{
addHiddenValue(parameterName, values[i]);
}
}
protected void writeTag(IMarkupWriter writer, String method, String url)
{
writer.begin("form");
writer.attribute("method", method);
writer.attribute("action", url);
}
public void prerenderField(IMarkupWriter writer, IComponent field, Location location)
{
Defense.notNull(writer, "writer");
Defense.notNull(field, "field");
String key = field.getExtendedId();
if (_prerenderMap.containsKey(key))
throw new ApplicationRuntimeException(FormMessages.fieldAlreadyPrerendered(field), field, location, null);
NestedMarkupWriter nested = writer.getNestedWriter();
TapestryUtils.storePrerender(_cycle, field);
_cycle.getResponseBuilder().render(nested, field, _cycle);
TapestryUtils.removePrerender(_cycle);
_prerenderMap.put(key, nested.getBuffer());
}
public boolean wasPrerendered(IMarkupWriter writer, IComponent field)
{
String key = field.getExtendedId();
// During a rewind, if the form is pre-rendered, the buffer will be null,
// so do the check based on the key, not a non-null value.
if (!_prerenderMap.containsKey(key))
return false;
String buffer = (String) _prerenderMap.get(key);
writer.printRaw(buffer);
_prerenderMap.remove(key);
return true;
}
public boolean wasPrerendered(IComponent field)
{
return _prerenderMap.containsKey(field.getExtendedId());
}
public void addDeferredRunnable(Runnable runnable)
{
Defense.notNull(runnable, "runnable");
_deferredRunnables.add(runnable);
}
public void registerForFocus(IFormComponent field, int priority)
{
_delegate.registerForFocus(field, priority);
}
/**
* {@inheritDoc}
*/
public JSONObject getProfile()
{
return _profile;
}
/**
* {@inheritDoc}
*/
public boolean isFormFieldUpdating()
{
return _fieldUpdating;
}
/**
* {@inheritDoc}
*/
public void setFormFieldUpdating(boolean value)
{
_fieldUpdating = value;
}
}