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}