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