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