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}