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