| // Copyright 2004 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 java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.tapestry.AbstractComponent; |
| import org.apache.tapestry.ApplicationRuntimeException; |
| import org.apache.tapestry.IActionListener; |
| import org.apache.tapestry.IBinding; |
| import org.apache.tapestry.IDirect; |
| import org.apache.tapestry.IEngine; |
| import org.apache.tapestry.IForm; |
| import org.apache.tapestry.IMarkupWriter; |
| import org.apache.tapestry.IRequestCycle; |
| import org.apache.tapestry.RenderRewoundException; |
| import org.apache.tapestry.StaleLinkException; |
| import org.apache.tapestry.Tapestry; |
| import org.apache.tapestry.engine.IEngineService; |
| import org.apache.tapestry.engine.ILink; |
| import org.apache.tapestry.html.Body; |
| import org.apache.tapestry.util.IdAllocator; |
| import org.apache.tapestry.util.StringSplitter; |
| import org.apache.tapestry.valid.IValidationDelegate; |
| |
| /** |
| * Component which contains form element components. Forms use the |
| * action or direct services to handle the form submission. A Form will wrap |
| * other components and static HTML, including |
| * form components such as {@link TextArea}, {@link TextField}, {@link Checkbox}, etc. |
| * |
| * [<a href="../../../../../ComponentReference/Form.html">Component Reference</a>] |
| * |
| * <p>When a form is submitted, it continues through the rewind cycle until |
| * <em>after</em> all of its wrapped elements have renderred. As the form |
| * component render (in the rewind cycle), they will be updating |
| * properties of the containing page and notifying thier listeners. Again: |
| * each form component is responsible not only for rendering HTML (to present the |
| * form), but for handling it's share of the form submission. |
| * |
| * <p>Only after all that is done will the Form notify its listener. |
| * |
| * <p>Starting in release 1.0.2, a Form can use either the direct service or |
| * the action service. The default is the direct service, even though |
| * in earlier releases, only the action service was available. |
| * |
| * @author Howard Lewis Ship, David Solis |
| * @version $Id$ |
| **/ |
| |
| public abstract class Form extends AbstractComponent implements IForm, IDirect |
| { |
| private static class HiddenValue |
| { |
| String _name; |
| String _value; |
| String _id; |
| |
| private HiddenValue(String name, String value) |
| { |
| this(name, null, value); |
| } |
| |
| private HiddenValue(String name, String id, String value) |
| { |
| _name = name; |
| _id = id; |
| _value = value; |
| } |
| } |
| |
| private boolean _rewinding; |
| private boolean _rendering; |
| private String _name; |
| |
| /** |
| * 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). |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| 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. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| private List _allocatedIds = new ArrayList(); |
| |
| /** |
| * {@link Map}, keyed on {@link FormEventType}. Values are either a String (the name |
| * of a single event), or a {@link List} of Strings. |
| * |
| * @since 1.0.2 |
| **/ |
| |
| private Map _events; |
| |
| private static final int EVENT_MAP_SIZE = 3; |
| |
| private IdAllocator _elementIdAllocator = new IdAllocator(); |
| |
| private String _encodingType; |
| |
| private List _hiddenValues; |
| |
| /** |
| * Returns the currently active {@link IForm}, or null if no form is |
| * active. This is a convienience method, the result will be |
| * null, or an instance of {@link IForm}, but not necessarily a |
| * <code>Form</code>. |
| * |
| **/ |
| |
| public static IForm get(IRequestCycle cycle) |
| { |
| return (IForm) cycle.getAttribute(ATTRIBUTE_NAME); |
| } |
| |
| /** |
| * Indicates to any wrapped form components that they should respond to the form |
| * submission. |
| * |
| * @throws ApplicationRuntimeException if not rendering. |
| **/ |
| |
| public boolean isRewinding() |
| { |
| if (!_rendering) |
| throw Tapestry.createRenderOnlyPropertyException(this, "rewinding"); |
| |
| return _rewinding; |
| } |
| |
| /** |
| * Returns true if this Form is configured to use the direct |
| * service. |
| * |
| * <p>This is derived from the direct parameter, and defaults |
| * to true if not bound. |
| * |
| * @since 1.0.2 |
| **/ |
| |
| public abstract boolean isDirect(); |
| |
| /** |
| * Returns true if the stateful parameter is bound to |
| * a true value. If stateful is not bound, also returns |
| * the default, true. |
| * |
| * @since 1.0.1 |
| **/ |
| |
| public boolean getRequiresSession() |
| { |
| return isStateful(); |
| } |
| |
| /** |
| * 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. |
| * |
| * |
| * @since 1.0.2 |
| **/ |
| |
| public String getElementId(IFormComponent component) |
| { |
| return getElementId(component, component.getId()); |
| } |
| |
| /** |
| * Constructs a unique identifier from the base id. If possible, the |
| * id is used as-is. Otherwise, a unique identifier is appended |
| * to the id. |
| * |
| * <p>This method is provided simply so that some components |
| * ({@link ImageSubmit}) have more specific control over |
| * their names. |
| * |
| * @since 1.0.3 |
| * |
| **/ |
| |
| public String getElementId(IFormComponent component, String baseId) |
| { |
| String result = _elementIdAllocator.allocateId(baseId); |
| |
| if (_rewinding) |
| { |
| if (_allocatedIdIndex >= _allocatedIds.size()) |
| { |
| throw new StaleLinkException( |
| Tapestry.format( |
| "Form.too-many-ids", |
| getExtendedId(), |
| Integer.toString(_allocatedIds.size()), |
| component.getExtendedId()), |
| this); |
| } |
| |
| String expected = (String) _allocatedIds.get(_allocatedIdIndex); |
| |
| if (!result.equals(expected)) |
| throw new StaleLinkException( |
| Tapestry.format( |
| "Form.id-mismatch", |
| new Object[] { |
| getExtendedId(), |
| Integer.toString(_allocatedIdIndex + 1), |
| expected, |
| result, |
| component.getExtendedId()}), |
| this); |
| } |
| else |
| { |
| _allocatedIds.add(result); |
| } |
| |
| _allocatedIdIndex++; |
| |
| component.setName(result); |
| |
| return result; |
| } |
| |
| /** |
| * Returns the name generated for the form. This is used to faciliate |
| * components that write JavaScript and need to access the form or |
| * its contents. |
| * |
| * <p>This value is generated when the form renders, and is not cleared. |
| * If the Form is inside a {@link org.apache.tapestry.components.Foreach}, |
| * this will be the most recently |
| * generated name for the Form. |
| * |
| * <p>This property is exposed so that sophisticated applications can write |
| * JavaScript handlers for the form and components within the form. |
| * |
| * @see AbstractFormComponent#getName() |
| * |
| **/ |
| |
| public String getName() |
| { |
| return _name; |
| } |
| |
| /** @since 3.0 **/ |
| |
| protected void prepareForRender(IRequestCycle cycle) |
| { |
| super.prepareForRender(cycle); |
| |
| if (cycle.getAttribute(ATTRIBUTE_NAME) != null) |
| throw new ApplicationRuntimeException( |
| Tapestry.getMessage("Form.forms-may-not-nest"), |
| this, |
| null, |
| null); |
| |
| cycle.setAttribute(ATTRIBUTE_NAME, this); |
| } |
| |
| protected void cleanupAfterRender(IRequestCycle cycle) |
| { |
| _rendering = false; |
| |
| _allocatedIdIndex = 0; |
| _allocatedIds.clear(); |
| |
| _events = null; |
| |
| _elementIdAllocator.clear(); |
| |
| if (_hiddenValues != null) |
| _hiddenValues.clear(); |
| |
| cycle.removeAttribute(ATTRIBUTE_NAME); |
| |
| _encodingType = null; |
| |
| IValidationDelegate delegate = getDelegate(); |
| |
| if (delegate != null) |
| delegate.setFormComponent(null); |
| |
| super.cleanupAfterRender(cycle); |
| } |
| |
| protected void writeAttributes(IMarkupWriter writer, ILink link) |
| { |
| String method = getMethod(); |
| |
| writer.begin(getTag()); |
| writer.attribute("method", (method == null) ? "post" : method); |
| writer.attribute("name", _name); |
| writer.attribute("action", link.getURL(null, false)); |
| |
| if (_encodingType != null) |
| writer.attribute("enctype", _encodingType); |
| } |
| |
| protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) |
| { |
| String actionId = cycle.getNextActionId(); |
| _name = getDisplayName() + actionId; |
| |
| boolean renderForm = !cycle.isRewinding(); |
| boolean rewound = cycle.isRewound(this); |
| |
| _rewinding = rewound; |
| |
| _allocatedIdIndex = 0; |
| |
| _rendering = true; |
| |
| if (rewound) |
| { |
| String storedIdList = cycle.getRequestContext().getParameter(_name); |
| |
| reconstructAllocatedIds(storedIdList); |
| } |
| |
| ILink link = getLink(cycle, actionId); |
| |
| // When rendering, use a nested writer so that an embedded Upload |
| // component can force the encoding type. |
| |
| IMarkupWriter nested = writer.getNestedWriter(); |
| |
| renderBody(nested, cycle); |
| |
| if (renderForm) |
| { |
| writeAttributes(writer, link); |
| |
| renderInformalParameters(writer, cycle); |
| writer.println(); |
| } |
| |
| // Write the hidden's, or at least, reserve the query parameters |
| // required by the Gesture. |
| |
| writeLinkParameters(writer, link, !renderForm); |
| |
| if (renderForm) |
| { |
| // What's this for? It's part of checking for stale links. |
| // We record the list of allocated ids. |
| // On rewind, we check that the stored list against which |
| // ids were allocated. If the persistent state of the page or |
| // application changed between render (previous request cycle) |
| // and rewind (current request cycle), then the list |
| // of ids will change as well. |
| |
| writeHiddenField(writer, _name, buildAllocatedIdList()); |
| writeHiddenValues(writer); |
| |
| nested.close(); |
| |
| writer.end(getTag()); |
| |
| // Write out event handlers collected during the rendering. |
| |
| emitEventHandlers(writer, cycle); |
| } |
| |
| if (rewound) |
| { |
| 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( |
| Tapestry.format( |
| "Form.too-few-ids", |
| getExtendedId(), |
| Integer.toString(expected - _allocatedIdIndex), |
| nextExpectedId), |
| this); |
| } |
| |
| IActionListener listener = getListener(); |
| |
| if (listener != null) |
| listener.actionTriggered(this, cycle); |
| |
| // Abort the rewind render. |
| |
| throw new RenderRewoundException(this); |
| } |
| } |
| |
| /** |
| * Adds an additional event handler. |
| * |
| * @since 1.0.2 |
| * |
| **/ |
| |
| public void addEventHandler(FormEventType type, String functionName) |
| { |
| if (_events == null) |
| _events = new HashMap(EVENT_MAP_SIZE); |
| |
| Object value = _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 (value == null) |
| { |
| _events.put(type, functionName); |
| return; |
| } |
| |
| // The second function added converts it to a List. |
| |
| if (value instanceof String) |
| { |
| List list = new ArrayList(); |
| list.add(value); |
| list.add(functionName); |
| |
| _events.put(type, list); |
| return; |
| } |
| |
| // The third and subsequent function just |
| // adds to the List. |
| |
| List list = (List) value; |
| list.add(functionName); |
| } |
| |
| protected void emitEventHandlers(IMarkupWriter writer, IRequestCycle cycle) |
| { |
| |
| if (_events == null || _events.isEmpty()) |
| return; |
| |
| Body body = Body.get(cycle); |
| |
| if (body == null) |
| throw new ApplicationRuntimeException( |
| Tapestry.getMessage("Form.needs-body-for-event-handlers"), |
| this, |
| null, |
| null); |
| |
| 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("document."); |
| buffer.append(_name); |
| buffer.append("."); |
| buffer.append(type.getPropertyName()); |
| buffer.append(" = "); |
| |
| // The typical case; one event one event handler. Easy enough. |
| |
| if (value instanceof String) |
| { |
| buffer.append(value.toString()); |
| buffer.append(";"); |
| } |
| else |
| { |
| // Build a composite function in-place |
| |
| buffer.append("function ()\n{\n"); |
| |
| boolean combineWithAnd = type.getCombineUsingAnd(); |
| |
| List l = (List) value; |
| int count = l.size(); |
| |
| for (int j = 0; j < count; j++) |
| { |
| String functionName = (String) l.get(j); |
| |
| if (j > 0) |
| { |
| |
| if (combineWithAnd) |
| buffer.append(" &&"); |
| else |
| buffer.append(";"); |
| } |
| |
| buffer.append("\n "); |
| |
| if (combineWithAnd) |
| { |
| if (j == 0) |
| buffer.append("return "); |
| else |
| buffer.append(" "); |
| } |
| |
| buffer.append(functionName); |
| buffer.append("()"); |
| } |
| |
| buffer.append(";\n}"); |
| } |
| |
| buffer.append("\n\n"); |
| } |
| |
| body.addInitializationScript(buffer.toString()); |
| } |
| |
| /** |
| * Simply invokes {@link #render(IMarkupWriter, IRequestCycle)}. |
| * |
| * @since 1.0.2 |
| * |
| **/ |
| |
| public void rewind(IMarkupWriter writer, IRequestCycle cycle) |
| { |
| render(writer, cycle); |
| } |
| |
| /** |
| * Method invoked by the direct service. |
| * |
| * @since 1.0.2 |
| * |
| **/ |
| |
| public void trigger(IRequestCycle cycle) |
| { |
| Object[] parameters = cycle.getServiceParameters(); |
| |
| cycle.rewindForm(this, (String) parameters[0]); |
| } |
| |
| /** |
| * Builds the EngineServiceLink for the form, using either the direct or |
| * action service. |
| * |
| * @since 1.0.3 |
| * |
| **/ |
| |
| private ILink getLink(IRequestCycle cycle, String actionId) |
| { |
| String serviceName = null; |
| |
| if (isDirect()) |
| serviceName = Tapestry.DIRECT_SERVICE; |
| else |
| serviceName = Tapestry.ACTION_SERVICE; |
| |
| IEngine engine = cycle.getEngine(); |
| IEngineService service = engine.getService(serviceName); |
| |
| // A single service parameter is used to store the actionId. |
| |
| return service.getLink(cycle, this, new String[] { actionId }); |
| } |
| |
| private void writeLinkParameters(IMarkupWriter writer, ILink link, boolean reserveOnly) |
| { |
| String[] names = link.getParameterNames(); |
| int count = Tapestry.size(names); |
| |
| for (int i = 0; i < count; i++) |
| { |
| String name = names[i]; |
| |
| // Reserve the name. |
| |
| _elementIdAllocator.allocateId(name); |
| |
| if (!reserveOnly) |
| writeHiddenFieldsForParameter(writer, link, name); |
| } |
| } |
| |
| /** |
| * @since 3.0 |
| * |
| **/ |
| |
| protected void writeHiddenField(IMarkupWriter writer, String name, String value) |
| { |
| writeHiddenField(writer, name, null, value); |
| } |
| |
| protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value) |
| { |
| writer.beginEmpty("input"); |
| writer.attribute("type", "hidden"); |
| writer.attribute("name", name); |
| |
| if(id != null && id.length() != 0) |
| writer.attribute("id", id); |
| |
| writer.attribute("value", value); |
| writer.println(); |
| } |
| |
| /** |
| * @since 2.2 |
| * |
| **/ |
| |
| private void writeHiddenFieldsForParameter( |
| IMarkupWriter writer, |
| ILink link, |
| String parameterName) |
| { |
| String[] values = link.getParameterValues(parameterName); |
| |
| for (int i = 0; i < values.length; i++) |
| { |
| writeHiddenField(writer, parameterName, values[i]); |
| } |
| } |
| |
| /** |
| * 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. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| protected 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(); |
| } |
| |
| /** |
| * Converts a string passed as a parameter (and containing a comma |
| * separated list of ids) back into the allocateIds property. |
| * |
| * @see #buildAllocatedIdList() |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| protected void reconstructAllocatedIds(String storedIdList) |
| { |
| if (Tapestry.isBlank(storedIdList)) |
| return; |
| |
| StringSplitter splitter = new StringSplitter(','); |
| |
| String[] ids = splitter.splitToArray(storedIdList); |
| |
| for (int i = 0; i < ids.length; i++) |
| _allocatedIds.add(ids[i]); |
| } |
| |
| public abstract IValidationDelegate getDelegate(); |
| |
| public abstract void setDelegate(IValidationDelegate delegate); |
| |
| public abstract void setDirect(boolean direct); |
| |
| public abstract IActionListener getListener(); |
| |
| public abstract String getMethod(); |
| |
| /** |
| * Invoked when not rendering, so it uses the stateful binding. |
| * If not bound, returns true. |
| * |
| **/ |
| |
| public boolean isStateful() |
| { |
| IBinding statefulBinding = getStatefulBinding(); |
| |
| if (statefulBinding == null) |
| return true; |
| |
| return statefulBinding.getBoolean(); |
| } |
| |
| public abstract IBinding getStatefulBinding(); |
| |
| protected void finishLoad() |
| { |
| setDirect(true); |
| } |
| |
| public void setEncodingType(String encodingType) |
| { |
| if (_encodingType != null && !_encodingType.equals(encodingType)) |
| throw new ApplicationRuntimeException( |
| Tapestry.format( |
| "Form.encoding-type-contention", |
| getExtendedId(), |
| _encodingType, |
| encodingType), |
| this, |
| null, |
| null); |
| |
| _encodingType = encodingType; |
| } |
| |
| /** |
| * Returns the tag of the form. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| protected String getTag() |
| { |
| return "form"; |
| } |
| |
| /** |
| * Returns the name of the element. |
| * |
| * |
| * @since 3.0 |
| **/ |
| |
| protected String getDisplayName() |
| { |
| return "Form"; |
| } |
| |
| /** @since 3.0 */ |
| |
| public void addHiddenValue(String name, String value) |
| { |
| if (_hiddenValues == null) |
| _hiddenValues = new ArrayList(); |
| |
| _hiddenValues.add(new HiddenValue(name, value)); |
| } |
| |
| /** @since 3.0 */ |
| |
| public void addHiddenValue(String name, String id, String value) |
| { |
| if (_hiddenValues == null) |
| _hiddenValues = new ArrayList(); |
| |
| _hiddenValues.add(new HiddenValue(name, id, value)); |
| } |
| |
| /** |
| * Writes hidden values accumulated during the render |
| * (by components invoking {@link #addHiddenValue(String, String)}. |
| * |
| * @since 3.0 |
| */ |
| |
| protected void writeHiddenValues(IMarkupWriter writer) |
| { |
| int count = Tapestry.size(_hiddenValues); |
| |
| for (int i = 0; i < count; i++) |
| { |
| HiddenValue hv = (HiddenValue) _hiddenValues.get(i); |
| |
| writeHiddenField(writer, hv._name, hv._id, hv._value); |
| } |
| } |
| } |