    001// Licensed under the Apache License, Version 2.0 (the "License");
    002// you may not use this file except in compliance with the License.
    003// You may obtain a copy of the License at
    004//
    005// http://www.apache.org/licenses/LICENSE-2.0
    006//
    007// Unless required by applicable law or agreed to in writing, software
    008// distributed under the License is distributed on an "AS IS" BASIS,
    009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    010// See the License for the specific language governing permissions and
    011// limitations under the License.
    012
    013package org.apache.tapestry5.corelib.components;
    014
    015import org.apache.tapestry5.*;
    016import org.apache.tapestry5.annotations.*;
    017import org.apache.tapestry5.corelib.ClientValidation;
    018import org.apache.tapestry5.corelib.internal.ComponentActionSink;
    019import org.apache.tapestry5.corelib.internal.FormSupportImpl;
    020import org.apache.tapestry5.corelib.internal.InternalFormSupport;
    021import org.apache.tapestry5.dom.Element;
    022import org.apache.tapestry5.internal.BeanValidationContext;
    023import org.apache.tapestry5.internal.BeanValidationContextImpl;
    024import org.apache.tapestry5.internal.InternalConstants;
    025import org.apache.tapestry5.internal.services.FormControlNameManager;
    026import org.apache.tapestry5.internal.services.HeartbeatImpl;
    027import org.apache.tapestry5.internal.util.AutofocusValidationDecorator;
    028import org.apache.tapestry5.ioc.Location;
    029import org.apache.tapestry5.ioc.Messages;
    030import org.apache.tapestry5.ioc.annotations.Inject;
    031import org.apache.tapestry5.ioc.annotations.Symbol;
    032import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    033import org.apache.tapestry5.ioc.internal.util.TapestryException;
    034import org.apache.tapestry5.ioc.services.PropertyAccess;
    035import org.apache.tapestry5.ioc.util.ExceptionUtils;
    036import org.apache.tapestry5.ioc.util.IdAllocator;
    037import org.apache.tapestry5.json.JSONArray;
    038import org.apache.tapestry5.runtime.Component;
    039import org.apache.tapestry5.services.*;
    040import org.apache.tapestry5.services.compatibility.DeprecationWarning;
    041import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    042import org.slf4j.Logger;
    043
    044import java.io.EOFException;
    045import java.io.IOException;
    046import java.io.ObjectInputStream;
    047import java.io.UnsupportedEncodingException;
    048import java.net.URLDecoder;
    049
    050/**
    051 * An HTML form, which will enclose other components to render out the various
    052 * types of fields.
    053 *
    054 * A Form triggers many notification events. When it renders, it triggers a
    055 * {@link org.apache.tapestry5.EventConstants#PREPARE_FOR_RENDER} notification, followed by a
    056 * {@link EventConstants#PREPARE} notification.
    057 *
    058 * When the form is submitted, the component triggers several notifications: first a
    059 * {@link EventConstants#PREPARE_FOR_SUBMIT}, then a {@link EventConstants#PREPARE}: these allow the page to update its
    060 * state as necessary to prepare for the form submission.
    061 *
    062 * The Form component then determines if the form was cancelled (see {@link org.apache.tapestry5.corelib.SubmitMode#CANCEL}). If so,
    063 * a {@link EventConstants#CANCELED} event is triggered.
    064 *
    065 * Next come notifications to contained components (or more accurately, the execution of stored {@link ComponentAction}s), to allow each component to retrieve and validate
    066 * submitted values, and update server-side properties.  This is based on the {@code t:formdata} query parameter,
    067 * which contains serialized object data (generated when the form initially renders).
    068 *
    069 * Once the form data is processed, the next step is to trigger the
    070 * {@link EventConstants#VALIDATE}, which allows for cross-form validation. After that, either a
    071 * {@link EventConstants#SUCCESS} OR {@link EventConstants#FAILURE} event (depending on whether the
    072 * {@link ValidationTracker} has recorded any errors). Lastly, a {@link EventConstants#SUBMIT} event, for any listeners
    073 * that care only about form submission, regardless of success or failure.
    074 *
    075 * For all of these notifications, the event context is derived from the <strong>context</strong> component parameter. This
    076 * context is encoded into the form's action URI (the parameter is not read when the form is submitted, instead the
    077 * values encoded into the form are used).
    078 *
    079 *
    080 * While rendering, or processing a Form submission, the Form component places a {@link FormSupport} object into the {@linkplain Environment environment},
    081 * so that enclosed components can coordinate with the Form component. It also places a {@link ValidationTracker} into the environment during both render and submission.
    082 * During submission it also pushes a {@link Heartbeat} into the environment, which is {@link org.apache.tapestry5.services.Heartbeat#end() ended} just before
    083 * {@linkplain FormSupport#defer(Runnable) deferred FormSupport operations} are executed.
    084 *
    085 *
    086 * @tapestrydoc
    087 * @see BeanEditForm
    088 * @see Errors
    089 * @see FormFragment
    090 * @see Label
    091 */
    092@Events(
    093        {EventConstants.PREPARE_FOR_RENDER, EventConstants.PREPARE, EventConstants.PREPARE_FOR_SUBMIT,
    094                EventConstants.VALIDATE, EventConstants.SUBMIT, EventConstants.FAILURE, EventConstants.SUCCESS, EventConstants.CANCELED})
    095@SupportsInformalParameters
    096public class Form implements ClientElement, FormValidationControl
    097{
    098    /**
    099     * Query parameter name storing form data (the serialized commands needed to
    100     * process a form submission).
    101     */
    102    public static final String FORM_DATA = "t:formdata";
    103
    104    /**
    105     * Used by {@link Submit}, etc., to identify which particular client-side element (by element id)
    106     * was responsible for the submission. An empty hidden field is created, as needed, to store this value.
    107     * Starting in Tapestry 5.3, this is a JSONArray with two values: the client id followed by the client name.
    108     *
    109     * @since 5.2.0
    110     */
    111    public static final String SUBMITTING_ELEMENT_ID = "t:submit";
    112
    113    public static final StreamPageContent STREAM_ACTIVE_PAGE_CONTENT = new StreamPageContent().withoutActivation();
    114
    115    /**
    116     * The context for the link (optional parameter). This list of values will
    117     * be converted into strings and included in
    118     * the URI. The strings will be coerced back to whatever their values are
    119     * and made available to event handler
    120     * methods.
    121     */
    122    @Parameter
    123    private Object[] context;
    124
    125    /**
    126     * The object which will record user input and validation errors. When not using
    127     * the default behavior supplied by the Form component (an immediate re-render of the active
    128     * page when there are form validation errors), it is necessary to bind this parameter
    129     * to a persistent value that can be maintained until the active page is re-rendered. See
    130     * <a href="https://issues.apache.org/jira/browse/TAP5-1808">TAP5-1801</a>.
    131     */
    132    @Parameter("defaultTracker")
    133    protected ValidationTracker tracker;
    134
    135    @Inject
    136    @Symbol(SymbolConstants.FORM_CLIENT_LOGIC_ENABLED)
    137    private boolean clientLogicDefaultEnabled;
    138
    139    /**
    140     * Controls when client validation occurs on the client, if at all. Defaults to {@link ClientValidation#SUBMIT}.
    141     * {@link ClientValidation#BLUR} was the default, prior to Tapestry 5.4, but is no longer supported.
    142     */
    143    @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
    144    private ClientValidation clientValidation = clientLogicDefaultEnabled ? ClientValidation.SUBMIT
    145            : ClientValidation.NONE;
    146
    147    /**
    148     * If true (the default), then the JavaScript will be added to position the
    149     * cursor into the form. The field to
    150     * receive focus is the first rendered field that is in error, or required,
    151     * or present (in that order of priority).
    152     *
    153     * @see SymbolConstants#FORM_CLIENT_LOGIC_ENABLED
    154     */
    155    @Parameter
    156    private boolean autofocus = clientLogicDefaultEnabled;
    157
    158    /**
    159     * Binding the zone parameter will cause the form submission to be handled
    160     * as an Ajax request that updates the
    161     * indicated zone. Often a Form will update the same zone that contains it.
    162     */
    163    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    164    private String zone;
    165
    166    /**
    167     * If true, then the Form's action will be secure (using an absolute URL with the HTTPs scheme) regardless
    168     * of whether the containing page itself is secure or not. This parameter does nothing
    169     * when {@linkplain SymbolConstants#SECURE_ENABLED security is disabled} (which is often
    170     * the case in development mode). This only affects how the Form's action attribute is rendered, there is
    171     * not (currently) a check that the form is actually submitted securely.
    172     */
    173    @Parameter
    174    private boolean secure;
    175
    176    /**
    177     * Prefix value used when searching for validation messages and constraints.
    178     * The default is the Form component's
    179     * id. This is overridden by {@link org.apache.tapestry5.corelib.components.BeanEditForm}.
    180     *
    181     * @see org.apache.tapestry5.services.FormSupport#getFormValidationId()
    182     */
    183    @Parameter
    184    private String validationId;
    185
    186    /**
    187     * Object to validate during the form submission process. The default is the Form component's container.
    188     * This parameter should only be used in combination with the Bean Validation Library.
    189     */
    190    @Parameter
    191    private Object validate;
    192
    193    /**
    194     * When true, the the form will submit as an asynchronous request (via XmlHttpRequest); the event handler methods
    195     * can make use of the {@link org.apache.tapestry5.services.ajax.AjaxResponseRenderer} in order to force content
    196     * updates to the client.  This is used as an alternative to placing the form inside a {@link org.apache.tapestry5.corelib.components.Zone}
    197     * and binding the {@code zone} parameter.
    198     *
    199     * @since 5.4
    200     */
    201    @Parameter
    202    private boolean async = false;
    203
    204    @Inject
    205    private Logger logger;
    206
    207    @Inject
    208    private Environment environment;
    209
    210    @Inject
    211    private ComponentResources resources;
    212
    213    @Inject
    214    private Messages messages;
    215
    216    @Environmental
    217    private JavaScriptSupport javascriptSupport;
    218
    219    @Inject
    220    private Request request;
    221
    222    @Inject
    223    private ComponentSource source;
    224
    225    @Inject
    226    private FormControlNameManager formControlNameManager;
    227
    228
    229    /**
    230     * Starting in 5.4, this is a simple, non-persistent property, with no extra magic tricks.
    231     */
    232    private ValidationTracker defaultTracker;
    233
    234    @Inject
    235    @Symbol(SymbolConstants.SECURE_ENABLED)
    236    private boolean secureEnabled;
    237
    238    private InternalFormSupport formSupport;
    239
    240    private Element form;
    241
    242    private Element div;
    243
    244    // Collects a stream of component actions. Each action goes in as a UTF
    245    // string (the component
    246    // component id), followed by a ComponentAction
    247
    248    private ComponentActionSink actionSink;
    249
    250    @SuppressWarnings("unchecked")
    251    @Environmental
    252    private TrackableComponentEventCallback eventCallback;
    253
    254    @Inject
    255    private ClientDataEncoder clientDataEncoder;
    256
    257    @Inject
    258    private PropertyAccess propertyAccess;
    259
    260    @Inject
    261    private DeprecationWarning deprecationWarning;
    262
    263    private String clientId;
    264
    265    @Inject
    266    private ComponentSource componentSource;
    267
    268    String defaultValidationId()
    269    {
    270        return resources.getId();
    271    }
    272
    273    Object defaultValidate()
    274    {
    275        return resources.getContainer();
    276    }
    277
    278    /**
    279     * Returns an instance of {@link ValidationTrackerImpl}, lazily creating it as needed. This property
    280     * is the default for the <strong>tracker</strong> parameter; the property (as of Tapestry 5.4) is not
    281     * persistent.
    282     *
    283     * @return per-request cached instance
    284     */
    285    public ValidationTracker getDefaultTracker()
    286    {
    287        if (defaultTracker == null)
    288        {
    289            defaultTracker = new ValidationTrackerImpl();
    290        }
    291
    292        return defaultTracker;
    293    }
    294
    295    /**
    296     * @deprecated In 5.4; previously used only for testing
    297     */
    298    public void setDefaultTracker(ValidationTracker defaultTracker)
    299    {
    300        this.defaultTracker = defaultTracker;
    301    }
    302
    303    void setupRender()
    304    {
    305        FormSupport existing = environment.peek(FormSupport.class);
    306
    307        if (existing != null)
    308        {
    309            throw new TapestryException(messages.get("core-form-nesting-not-allowed"), existing, null);
    310        }
    311
    312        if (clientValidation == ClientValidation.BLUR)
    313        {
    314            deprecationWarning.componentParameterValue(resources, "clientValidation", clientValidation, "BLUR is no longer supported, starting in 5.4. Validation will occur as with SUBMIT.");
    315        }
    316    }
    317
    318    void beginRender(MarkupWriter writer)
    319    {
    320        Link link = resources.createFormEventLink(EventConstants.ACTION, context);
    321
    322        String actionURL = secure && secureEnabled ? link.toAbsoluteURI(true) : link.toURI();
    323
    324        actionSink = new ComponentActionSink(logger, clientDataEncoder);
    325
    326        clientId = javascriptSupport.allocateClientId(resources);
    327
    328        // Pre-register some names, to prevent client-side collisions with function names
    329        // attached to the JS Form object.
    330
    331        IdAllocator allocator = new IdAllocator();
    332
    333        preallocateNames(allocator);
    334
    335        formSupport = createRenderTimeFormSupport(clientId, actionSink, allocator);
    336
    337        environment.push(FormSupport.class, formSupport);
    338        environment.push(ValidationTracker.class, tracker);
    339
    340        if (autofocus)
    341        {
    342            ValidationDecorator autofocusDecorator = new AutofocusValidationDecorator(
    343                    environment.peek(ValidationDecorator.class), tracker, javascriptSupport);
    344            environment.push(ValidationDecorator.class, autofocusDecorator);
    345        }
    346
    347        // Now that the environment is setup, inform the component or other
    348        // listeners that the form
    349        // is about to render.
    350
    351        resources.triggerEvent(EventConstants.PREPARE_FOR_RENDER, context, null);
    352
    353        resources.triggerEvent(EventConstants.PREPARE, context, null);
    354
    355        // Push BeanValidationContext only after the container had a chance to prepare
    356        environment.push(BeanValidationContext.class, new BeanValidationContextImpl(validate));
    357
    358        // Save the form element for later, in case we want to write an encoding
    359        // type attribute.
    360
    361        form = writer.element("form",
    362                "id", clientId,
    363                "method", "post",
    364                "action", actionURL,
    365                "data-update-zone", zone);
    366
    367        if (clientValidation != ClientValidation.NONE)
    368        {
    369            writer.attributes("data-validate", "submit");
    370        }
    371
    372        if (async)
    373        {
    374            javascriptSupport.require("t5/core/zone");
    375            writer.attributes("data-async-trigger", true);
    376        }
    377
    378        resources.renderInformalParameters(writer);
    379
    380        div = writer.element("div");
    381
    382        for (String parameterName : link.getParameterNames())
    383        {
    384            String[] values = link.getParameterValues(parameterName);
    385
    386            for (String value : values)
    387            {
    388                // The parameter value is expected to be encoded,
    389                // but the input value shouldn't be encoded.
    390                try
    391                {
    392                    value = URLDecoder.decode(value, "UTF-8");
    393                } catch (UnsupportedEncodingException e)
    394                {
    395                    logger.error(String.format(
    396                            "Enable to decode parameter value for parameter %s in form %s",
    397                            parameterName, form.getName()), e);
    398                }
    399                writer.element("input", "type", "hidden", "name", parameterName, "value", value);
    400                writer.end();
    401            }
    402        }
    403
    404        writer.end(); // div
    405
    406        environment.peek(Heartbeat.class).begin();
    407    }
    408
    409    /**
    410     * Creates an {@link org.apache.tapestry5.corelib.internal.InternalFormSupport} for
    411     * this Form.
    412     *
    413     * This method may also be invoked as the handler for the "internalCreateRenderTimeFormSupport" event.
    414     *
    415     * @param clientId
    416     *         the client-side id for the rendered form
    417     *         element
    418     * @param actionSink
    419     *         used to collect component actions that will, ultimately, be
    420     *         written as the t:formdata hidden
    421     *         field
    422     * @param allocator
    423     *         used to allocate unique ids
    424     * @return form support object
    425     */
    426    @OnEvent("internalCreateRenderTimeFormSupport")
    427    InternalFormSupport createRenderTimeFormSupport(String clientId, ComponentActionSink actionSink,
    428                                                    IdAllocator allocator)
    429    {
    430        return new FormSupportImpl(resources, clientId, actionSink,
    431                clientValidation != ClientValidation.NONE, allocator, validationId);
    432    }
    433
    434    void afterRender(MarkupWriter writer)
    435    {
    436        environment.peek(Heartbeat.class).end();
    437
    438        formSupport.executeDeferred();
    439
    440        String encodingType = formSupport.getEncodingType();
    441
    442        if (encodingType != null)
    443        {
    444            form.forceAttributes("enctype", encodingType);
    445        }
    446
    447        writer.end(); // form
    448
    449        div.element("input", "type", "hidden", "name", FORM_DATA, "value", actionSink.getClientData());
    450        div.pop();
    451
    452        if (autofocus)
    453        {
    454            environment.pop(ValidationDecorator.class);
    455        }
    456    }
    457
    458    void cleanupRender()
    459    {
    460        environment.pop(FormSupport.class);
    461
    462        formSupport = null;
    463
    464        environment.pop(ValidationTracker.class);
    465
    466        tracker.clear();
    467
    468        environment.pop(BeanValidationContext.class);
    469    }
    470
    471    @SuppressWarnings(
    472            {"unchecked", "InfiniteLoopStatement"})
    473    Object onAction(EventContext context) throws IOException
    474    {
    475        beforeProcessSubmit(context);
    476
    477        tracker.clear();
    478
    479        formSupport = new FormSupportImpl(resources, validationId);
    480
    481        environment.push(ValidationTracker.class, tracker);
    482        environment.push(FormSupport.class, formSupport);
    483
    484        Heartbeat heartbeat = new HeartbeatImpl();
    485
    486        environment.push(Heartbeat.class, heartbeat);
    487
    488        heartbeat.begin();
    489
    490        boolean didPushBeanValidationContext = false;
    491
    492        try
    493        {
    494            resources.triggerContextEvent(EventConstants.PREPARE_FOR_SUBMIT, context, eventCallback);
    495
    496            if (eventCallback.isAborted())
    497                return true;
    498
    499            resources.triggerContextEvent(EventConstants.PREPARE, context, eventCallback);
    500            if (eventCallback.isAborted())
    501                return true;
    502
    503            if (isFormCancelled())
    504            {
    505                executeStoredActions(true);
    506
    507                resources.triggerContextEvent(EventConstants.CANCELED, context, eventCallback);
    508                if (eventCallback.isAborted())
    509                    return true;
    510            }
    511
    512            environment.push(BeanValidationContext.class, new BeanValidationContextImpl(validate));
    513
    514            didPushBeanValidationContext = true;
    515
    516            executeStoredActions(false);
    517
    518            heartbeat.end();
    519
    520            formSupport.executeDeferred();
    521
    522            fireValidateEvent(EventConstants.VALIDATE, context, eventCallback);
    523
    524            if (eventCallback.isAborted())
    525            {
    526                return true;
    527            }
    528
    529            afterValidate();
    530
    531            // Let the listeners know about overall success or failure. Most
    532            // listeners fall into
    533            // one of those two camps.
    534
    535            // If the tracker has no errors, then clear it of any input values
    536            // as well, so that the next page render will be "clean" and show
    537            // true persistent data, not value from the previous form
    538            // submission.
    539
    540            if (!tracker.getHasErrors())
    541            {
    542                tracker.clear();
    543            }
    544
    545            String eventType = tracker.getHasErrors()
    546                    ? EventConstants.FAILURE
    547                    : EventConstants.SUCCESS;
    548
    549            resources.triggerContextEvent(eventType, context, eventCallback);
    550
    551            if (eventCallback.isAborted())
    552            {
    553                return true;
    554            }
    555
    556            // Lastly, tell anyone whose interested that the form is completely
    557            // submitted.
    558
    559            resources.triggerContextEvent(EventConstants.SUBMIT, context, eventCallback);
    560
    561            afterSuccessOrFailure();
    562
    563            if (eventCallback.isAborted())
    564            {
    565                return true;
    566            }
    567
    568            // For traditional request with no validation exceptions, re-render the
    569            // current page immediately, as-is.  Prior to Tapestry 5.4, a redirect was
    570            // sent that required that the tracker be persisted across requests.
    571            // See https://issues.apache.org/jira/browse/TAP5-1808
    572
    573            if (tracker.getHasErrors() && !request.isXHR())
    574            {
    575                return STREAM_ACTIVE_PAGE_CONTENT;
    576            }
    577
    578            // The event will not work its way up.
    579
    580            return false;
    581
    582        } finally
    583        {
    584            environment.pop(Heartbeat.class);
    585            environment.pop(FormSupport.class);
    586
    587            environment.pop(ValidationTracker.class);
    588
    589            if (didPushBeanValidationContext)
    590            {
    591                environment.pop(BeanValidationContext.class);
    592            }
    593        }
    594    }
    595
    596    /**
    597     * A hook invoked from {@link #onAction(org.apache.tapestry5.EventContext)} after the
    598     * {@link org.apache.tapestry5.EventConstants#SUBMIT} or {@link org.apache.tapestry5.EventConstants#FAILURE} event has been triggered.
    599     *
    600     * This method will be invoked regardless of whether the submit or failure event was aborted.
    601     *
    602     * This implementation does nothing.
    603     *
    604     * @since 5.4
    605     */
    606
    607    protected void afterSuccessOrFailure()
    608    {
    609
    610    }
    611
    612    /**
    613     * A hook invoked from {@link #onAction(org.apache.tapestry5.EventContext)} before any other setup.
    614     *
    615     * This implementation does nothing.
    616     *
    617     * @param context
    618     *         as passed to {@code onAction()}
    619     * @since 5.4
    620     */
    621    protected void beforeProcessSubmit(EventContext context)
    622    {
    623
    624    }
    625
    626    /**
    627     * A hook invoked from {@link #onAction(org.apache.tapestry5.EventContext)} after the
    628     * {@link org.apache.tapestry5.EventConstants#VALIDATE} event has been triggered, and
    629     * before the {@link #tracker} has been {@linkplain org.apache.tapestry5.ValidationTracker#clear() cleared}.
    630     *
    631     * Only invoked if the valiate event did not abort (that is, the no event handler method returned a value).
    632     *
    633     * This implementation does nothing.
    634     *
    635     * @since 5.4
    636     */
    637    protected void afterValidate()
    638    {
    639
    640    }
    641
    642    private boolean isFormCancelled()
    643    {
    644        // The "cancel" query parameter is reserved for this purpose; if it is present then the form was canceled on the
    645        // client side.  For image submits, there will be two parameters: "cancel.x" and "cancel.y".
    646
    647        if (request.getParameter(InternalConstants.CANCEL_NAME) != null ||
    648                request.getParameter(InternalConstants.CANCEL_NAME + ".x") != null)
    649        {
    650            return true;
    651        }
    652
    653        // When JavaScript is involved, it's more complicated. In fact, this is part of HLS's desire
    654        // to have all forms submit via XHR when JavaScript is present, since it would provide
    655        // an opportunity to get the submitting element's value into the request properly.
    656
    657        String raw = request.getParameter(SUBMITTING_ELEMENT_ID);
    658
    659        if (InternalUtils.isNonBlank(raw) &&
    660                new JSONArray(raw).getString(1).equals(InternalConstants.CANCEL_NAME))
    661        {
    662            return true;
    663        }
    664
    665        return false;
    666    }
    667
    668
    669    private void fireValidateEvent(String eventName, EventContext context, TrackableComponentEventCallback callback)
    670    {
    671        try
    672        {
    673            resources.triggerContextEvent(eventName, context, callback);
    674        } catch (RuntimeException ex)
    675        {
    676            ValidationException ve = ExceptionUtils.findCause(ex, ValidationException.class, propertyAccess);
    677
    678            if (ve != null)
    679            {
    680                ValidationTracker tracker = environment.peek(ValidationTracker.class);
    681
    682                tracker.recordError(ve.getMessage());
    683
    684                return;
    685            }
    686
    687            throw ex;
    688        }
    689    }
    690
    691    /**
    692     * Pulls the stored actions out of the request, converts them from MIME
    693     * stream back to object stream and then
    694     * objects, and executes them.
    695     */
    696    private void executeStoredActions(boolean forFormCancel)
    697    {
    698        String[] values = request.getParameters(FORM_DATA);
    699
    700        if (!request.getMethod().equals("POST") || values == null)
    701            throw new RuntimeException(messages.format("core-invalid-form-request", FORM_DATA));
    702
    703        // Due to Ajax there may be multiple values here, so
    704        // handle each one individually.
    705
    706        for (String clientEncodedActions : values)
    707        {
    708            if (InternalUtils.isBlank(clientEncodedActions))
    709                continue;
    710
    711            logger.debug("Processing actions: {}", clientEncodedActions);
    712
    713            ObjectInputStream ois = null;
    714
    715            Component component = null;
    716
    717            try
    718            {
    719                ois = clientDataEncoder.decodeClientData(clientEncodedActions);
    720
    721                while (!eventCallback.isAborted())
    722                {
    723                    String componentId = ois.readUTF();
    724                    boolean cancelAction = ois.readBoolean();
    725                    ComponentAction action = (ComponentAction) ois.readObject();
    726
    727                    // Actions are a mix of ordinary actions and cancel actions.  Filter out one set or the other
    728                    // based on whether the form was submitted or cancelled.
    729                    if (forFormCancel != cancelAction)
    730                    {
    731                        continue;
    732                    }
    733
    734                    component = source.getComponent(componentId);
    735
    736                    logger.debug("Processing: {} {}", componentId, action);
    737
    738                    action.execute(component);
    739
    740                    component = null;
    741                }
    742            } catch (EOFException ex)
    743            {
    744                // Expected
    745            } catch (Exception ex)
    746            {
    747                Location location = component == null ? null : component.getComponentResources().getLocation();
    748
    749                throw new TapestryException(ex.getMessage(), location, ex);
    750            } finally
    751            {
    752                InternalUtils.close(ois);
    753            }
    754        }
    755    }
    756
    757    public void recordError(String errorMessage)
    758    {
    759        tracker.recordError(errorMessage);
    760    }
    761
    762    public void recordError(Field field, String errorMessage)
    763    {
    764        tracker.recordError(field, errorMessage);
    765    }
    766
    767    public boolean getHasErrors()
    768    {
    769        return tracker.getHasErrors();
    770    }
    771
    772    public boolean isValid()
    773    {
    774        return !tracker.getHasErrors();
    775    }
    776
    777    public void clearErrors()
    778    {
    779        tracker.clear();
    780    }
    781
    782    // For testing:
    783
    784    void setTracker(ValidationTracker tracker)
    785    {
    786        this.tracker = tracker;
    787    }
    788
    789    /**
    790     * Forms use the same value for their name and their id attribute.
    791     */
    792    public String getClientId()
    793    {
    794        return clientId;
    795    }
    796
    797    private void preallocateNames(IdAllocator idAllocator)
    798    {
    799        for (String name : formControlNameManager.getReservedNames())
    800        {
    801            idAllocator.allocateId(name);
    802            // See https://issues.apache.org/jira/browse/TAP5-1632
    803            javascriptSupport.allocateClientId(name);
    804
    805        }
    806
    807        Component activePage = componentSource.getActivePage();
    808
    809        // This is unlikely but may be possible if people override some of the standard
    810        // exception reporting logic.
    811
    812        if (activePage == null)
    813            return;
    814
    815        ComponentResources activePageResources = activePage.getComponentResources();
    816
    817        try
    818        {
    819
    820            activePageResources.triggerEvent(EventConstants.PREALLOCATE_FORM_CONTROL_NAMES, new Object[]
    821                    {idAllocator}, null);
    822        } catch (RuntimeException ex)
    823        {
    824            logger.error(
    825                    String.format("Unable to obtain form control names to preallocate: %s",
    826                            ExceptionUtils.toMessage(ex)), ex);
    827        }
    828    }
    829}