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