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