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