    001    // Copyright 2006, 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.corelib.components;
    016
    017    import org.apache.tapestry5.*;
    018    import org.apache.tapestry5.annotations.*;
    019    import org.apache.tapestry5.corelib.ClientValidation;
    020    import org.apache.tapestry5.corelib.internal.ComponentActionSink;
    021    import org.apache.tapestry5.corelib.internal.FormSupportImpl;
    022    import org.apache.tapestry5.corelib.internal.InternalFormSupport;
    023    import org.apache.tapestry5.dom.Element;
    024    import org.apache.tapestry5.internal.*;
    025    import org.apache.tapestry5.internal.services.HeartbeatImpl;
    026    import org.apache.tapestry5.internal.util.AutofocusValidationDecorator;
    027    import org.apache.tapestry5.ioc.Location;
    028    import org.apache.tapestry5.ioc.Messages;
    029    import org.apache.tapestry5.ioc.annotations.Inject;
    030    import org.apache.tapestry5.ioc.annotations.Symbol;
    031    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    032    import org.apache.tapestry5.ioc.internal.util.TapestryException;
    033    import org.apache.tapestry5.ioc.services.PropertyAccess;
    034    import org.apache.tapestry5.ioc.util.ExceptionUtils;
    035    import org.apache.tapestry5.ioc.util.IdAllocator;
    036    import org.apache.tapestry5.json.JSONArray;
    037    import org.apache.tapestry5.json.JSONObject;
    038    import org.apache.tapestry5.runtime.Component;
    039    import org.apache.tapestry5.services.*;
    040    import org.apache.tapestry5.services.javascript.InitializationPriority;
    041    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    042    import org.slf4j.Logger;
    043
    044    import java.io.EOFException;
    045    import java.io.IOException;
    046    import java.io.ObjectInputStream;
    047    import java.io.UnsupportedEncodingException;
    048    import 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.
    082     * </p>
    083     *
    084     * @tapestrydoc
    085     * @see BeanEditForm
    086     * @see Errors
    087     * @see FormFragment
    088     * @see Label
    089     */
    090    @Events(
    091            {EventConstants.PREPARE_FOR_RENDER, EventConstants.PREPARE, EventConstants.PREPARE_FOR_SUBMIT,
    092                    EventConstants.VALIDATE, EventConstants.SUBMIT, EventConstants.FAILURE, EventConstants.SUCCESS, EventConstants.CANCELED})
    093    @SupportsInformalParameters
    094    public class Form implements ClientElement, FormValidationControl
    095    {
    096        /**
    097         * Query parameter name storing form data (the serialized commands needed to
    098         * process a form submission).
    099         */
    100        public static final String FORM_DATA = "t:formdata";
    101
    102        /**
    103         * Used by {@link Submit}, etc., to identify which particular client-side element (by element id)
    104         * was responsible for the submission. An empty hidden field is created, as needed, to store this value.
    105         * Starting in Tapestry 5.3, this is a JSONArray with two values: the client id followed by the client name.
    106         *
    107         * @since 5.2.0
    108         */
    109        public static final String SUBMITTING_ELEMENT_ID = "t:submit";
    110
    111        /**
    112         * The context for the link (optional parameter). This list of values will
    113         * be converted into strings and included in
    114         * the URI. The strings will be coerced back to whatever their values are
    115         * and made available to event handler
    116         * methods.
    117         */
    118        @Parameter
    119        private Object[] context;
    120
    121        /**
    122         * The object which will record user input and validation errors. The object
    123         * must be persistent between requests
    124         * (since the form submission and validation occurs in a component event
    125         * request and the subsequent render occurs
    126         * in a render request). The default is a persistent property of the Form
    127         * component and this is sufficient for
    128         * nearly all purposes (except when a Form is rendered inside a loop).
    129         */
    130        @Parameter("defaultTracker")
    131        private ValidationTracker tracker;
    132
    133        @Inject
    134        @Symbol(SymbolConstants.FORM_CLIENT_LOGIC_ENABLED)
    135        private boolean clientLogicDefaultEnabled;
    136
    137        /**
    138         * Controls when client validation occurs on the client, if at all. Defaults to {@link ClientValidation#BLUR}.
    139         */
    140        @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL)
    141        private ClientValidation clientValidation = clientLogicDefaultEnabled ? ClientValidation.BLUR
    142                : ClientValidation.NONE;
    143
    144        /**
    145         * If true (the default), then the JavaScript will be added to position the
    146         * cursor into the form. The field to
    147         * receive focus is the first rendered field that is in error, or required,
    148         * or present (in that order of priority).
    149         *
    150         * @see SymbolConstants#FORM_CLIENT_LOGIC_ENABLED
    151         */
    152        @Parameter
    153        private boolean autofocus = clientLogicDefaultEnabled;
    154
    155        /**
    156         * Binding the zone parameter will cause the form submission to be handled
    157         * as an Ajax request that updates the
    158         * indicated zone. Often a Form will update the same zone that contains it.
    159         */
    160        @Parameter(defaultPrefix = BindingConstants.LITERAL)
    161        private String zone;
    162
    163        /**
    164         * If true, then the Form's action will be secure (using an absolute URL with the HTTPs scheme) regardless
    165         * of whether the containing page itself is secure or not. This parameter does nothing
    166         * when {@linkplain SymbolConstants#SECURE_ENABLED security is disabled} (which is often
    167         * the case in development mode). This only affects how the Form's action attribute is rendered, there is
    168         * not (currently) a check that the form is actually submitted securely.
    169         */
    170        @Parameter
    171        private boolean secure;
    172
    173        /**
    174         * Prefix value used when searching for validation messages and constraints.
    175         * The default is the Form component's
    176         * id. This is overridden by {@link org.apache.tapestry5.corelib.components.BeanEditForm}.
    177         *
    178         * @see org.apache.tapestry5.services.FormSupport#getFormValidationId()
    179         */
    180        @Parameter
    181        private String validationId;
    182
    183        /**
    184         * Object to validate during the form submission process. The default is the Form component's container.
    185         * This parameter should only be used in combination with the Bean Validation Library.
    186         */
    187        @Parameter
    188        private Object validate;
    189
    190        @Inject
    191        private Logger logger;
    192
    193        @Inject
    194        private Environment environment;
    195
    196        @Inject
    197        private ComponentResources resources;
    198
    199        @Inject
    200        private Messages messages;
    201
    202        @Environmental
    203        private JavaScriptSupport javascriptSupport;
    204
    205        @Environmental
    206        private JavaScriptSupport jsSupport;
    207
    208        @Inject
    209        private Request request;
    210
    211        @Inject
    212        private ComponentSource source;
    213
    214        @Inject
    215        @Symbol(InternalSymbols.PRE_SELECTED_FORM_NAMES)
    216        private String preselectedFormNames;
    217
    218        @Persist(PersistenceConstants.FLASH)
    219        private ValidationTracker defaultTracker;
    220
    221        @Inject
    222        @Symbol(SymbolConstants.SECURE_ENABLED)
    223        private boolean secureEnabled;
    224
    225        private InternalFormSupport formSupport;
    226
    227        private Element form;
    228
    229        private Element div;
    230
    231        // Collects a stream of component actions. Each action goes in as a UTF
    232        // string (the component
    233        // component id), followed by a ComponentAction
    234
    235        private ComponentActionSink actionSink;
    236
    237        @Environmental
    238        private ClientBehaviorSupport clientBehaviorSupport;
    239
    240        @SuppressWarnings("unchecked")
    241        @Environmental
    242        private TrackableComponentEventCallback eventCallback;
    243
    244        @Inject
    245        private ClientDataEncoder clientDataEncoder;
    246
    247        @Inject
    248        private PropertyAccess propertyAccess;
    249
    250        private String clientId;
    251
    252        // Set during rendering or submit processing to be the
    253        // same as the VT pushed into the Environment
    254        private ValidationTracker activeTracker;
    255
    256        String defaultValidationId()
    257        {
    258            return resources.getId();
    259        }
    260
    261        Object defaultValidate()
    262        {
    263            return resources.getContainer();
    264        }
    265
    266        /**
    267         * Returns a wrapped version of the tracker parameter (which is usually bound to the
    268         * defaultTracker persistent field).
    269         * If tracker is currently null, a new instance of {@link ValidationTrackerImpl} is created.
    270         * The tracker is then wrapped, such that the tracker parameter
    271         * is only updated the first time an error is recorded into the tracker (this will typically
    272         * propagate to the defaultTracker
    273         * persistent field and be stored into the session). This means that if no errors are recorded,
    274         * the tracker parameter is not updated and (in the default case) no data is stored into the
    275         * session.
    276         *
    277         * @return a tracker ready to receive data (possibly a previously stored tracker with field
    278         *         input and errors)
    279         * @see <a href="https://issues.apache.org/jira/browse/TAP5-979">TAP5-979</a>
    280         */
    281        private ValidationTracker getWrappedTracker()
    282        {
    283            ValidationTracker innerTracker = tracker == null ? new ValidationTrackerImpl() : tracker;
    284
    285            ValidationTracker wrapper = new ValidationTrackerWrapper(innerTracker)
    286            {
    287                private boolean saved = false;
    288
    289                private void save()
    290                {
    291                    if (!saved)
    292                    {
    293                        tracker = getDelegate();
    294
    295                        saved = true;
    296                    }
    297                }
    298
    299                @Override
    300                public void recordError(Field field, String errorMessage)
    301                {
    302                    super.recordError(field, errorMessage);
    303
    304                    save();
    305                }
    306
    307                @Override
    308                public void recordError(String errorMessage)
    309                {
    310                    super.recordError(errorMessage);
    311
    312                    save();
    313                }
    314            };
    315
    316            return wrapper;
    317        }
    318
    319        public ValidationTracker getDefaultTracker()
    320        {
    321            return defaultTracker;
    322        }
    323
    324        public void setDefaultTracker(ValidationTracker defaultTracker)
    325        {
    326            this.defaultTracker = defaultTracker;
    327        }
    328
    329        void setupRender()
    330        {
    331            FormSupport existing = environment.peek(FormSupport.class);
    332
    333            if (existing != null)
    334                throw new TapestryException(messages.get("nesting-not-allowed"), existing, null);
    335        }
    336
    337        void beginRender(MarkupWriter writer)
    338        {
    339            Link link = resources.createFormEventLink(EventConstants.ACTION, context);
    340
    341            String actionURL = secure && secureEnabled ? link.toAbsoluteURI(true) : link.toURI();
    342
    343            actionSink = new ComponentActionSink(logger, clientDataEncoder);
    344
    345            clientId = javascriptSupport.allocateClientId(resources);
    346
    347            // Pre-register some names, to prevent client-side collisions with function names
    348            // attached to the JS Form object.
    349
    350            IdAllocator allocator = new IdAllocator();
    351
    352            preallocateNames(allocator);
    353
    354            formSupport = createRenderTimeFormSupport(clientId, actionSink, allocator);
    355
    356            addJavaScriptInitialization();
    357
    358            if (zone != null)
    359                linkFormToZone(link);
    360
    361            activeTracker = getWrappedTracker();
    362
    363            environment.push(FormSupport.class, formSupport);
    364            environment.push(ValidationTracker.class, activeTracker);
    365
    366            if (autofocus)
    367            {
    368                ValidationDecorator autofocusDecorator = new AutofocusValidationDecorator(
    369                        environment.peek(ValidationDecorator.class), activeTracker, jsSupport);
    370                environment.push(ValidationDecorator.class, autofocusDecorator);
    371            }
    372
    373            // Now that the environment is setup, inform the component or other
    374            // listeners that the form
    375            // is about to render.
    376
    377            resources.triggerEvent(EventConstants.PREPARE_FOR_RENDER, context, null);
    378
    379            resources.triggerEvent(EventConstants.PREPARE, context, null);
    380
    381            // Push BeanValidationContext only after the container had a chance to prepare
    382            environment.push(BeanValidationContext.class, new BeanValidationContextImpl(validate));
    383
    384            // Save the form element for later, in case we want to write an encoding
    385            // type attribute.
    386
    387            form = writer.element("form", "id", clientId, "method", "post", "action", actionURL);
    388
    389            if ((zone != null || clientValidation != ClientValidation.NONE) && !request.isXHR())
    390                writer.attributes("onsubmit", MarkupConstants.WAIT_FOR_PAGE);
    391
    392            resources.renderInformalParameters(writer);
    393
    394            div = writer.element("div", "class", CSSClassConstants.INVISIBLE);
    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                    }
    408                    catch (UnsupportedEncodingException e)
    409                    {
    410                        logger.error(String.format(
    411                                "Enable to decode parameter value for parameter %s in form %s",
    412                                parameterName, form.getName()), e);
    413                    }
    414                    writer.element("input", "type", "hidden", "name", parameterName, "value", value);
    415                    writer.end();
    416                }
    417            }
    418
    419            writer.end(); // div
    420
    421            environment.peek(Heartbeat.class).begin();
    422        }
    423
    424        private void addJavaScriptInitialization()
    425        {
    426            JSONObject validateSpec = new JSONObject().put("blur", clientValidation == ClientValidation.BLUR).put("submit",
    427                    clientValidation != ClientValidation.NONE);
    428
    429            JSONObject spec = new JSONObject("formId", clientId).put("validate", validateSpec);
    430
    431            javascriptSupport.addInitializerCall(InitializationPriority.EARLY, "formEventManager", spec);
    432        }
    433
    434        @HeartbeatDeferred
    435        private void linkFormToZone(Link link)
    436        {
    437            clientBehaviorSupport.linkZone(clientId, zone, link);
    438        }
    439
    440        /**
    441         * Creates an {@link org.apache.tapestry5.corelib.internal.InternalFormSupport} for
    442         * this Form. This method is used
    443         * by {@link org.apache.tapestry5.corelib.components.FormInjector}.
    444         * <p/>
    445         * This method may also be invoked as the handler for the "internalCreateRenderTimeFormSupport" event.
    446         *
    447         * @param clientId   the client-side id for the rendered form
    448         *                   element
    449         * @param actionSink used to collect component actions that will, ultimately, be
    450         *                   written as the t:formdata hidden
    451         *                   field
    452         * @param allocator  used to allocate unique ids
    453         * @return form support object
    454         */
    455        @OnEvent("internalCreateRenderTimeFormSupport")
    456        InternalFormSupport createRenderTimeFormSupport(String clientId, ComponentActionSink actionSink,
    457                                                        IdAllocator allocator)
    458        {
    459            return new FormSupportImpl(resources, clientId, actionSink, clientBehaviorSupport,
    460                    clientValidation != ClientValidation.NONE, allocator, validationId);
    461        }
    462
    463        void afterRender(MarkupWriter writer)
    464        {
    465            environment.peek(Heartbeat.class).end();
    466
    467            formSupport.executeDeferred();
    468
    469            String encodingType = formSupport.getEncodingType();
    470
    471            if (encodingType != null)
    472                form.forceAttributes("enctype", encodingType);
    473
    474            writer.end(); // form
    475
    476            div.element("input", "type", "hidden", "name", FORM_DATA, "value", actionSink.getClientData());
    477
    478            if (autofocus)
    479                environment.pop(ValidationDecorator.class);
    480        }
    481
    482        void cleanupRender()
    483        {
    484            environment.pop(FormSupport.class);
    485
    486            formSupport = null;
    487
    488            environment.pop(ValidationTracker.class);
    489
    490            activeTracker = null;
    491
    492            environment.pop(BeanValidationContext.class);
    493        }
    494
    495        @SuppressWarnings(
    496                {"unchecked", "InfiniteLoopStatement"})
    497        @Log
    498        Object onAction(EventContext context) throws IOException
    499        {
    500            activeTracker = getWrappedTracker();
    501
    502            activeTracker.clear();
    503
    504            formSupport = new FormSupportImpl(resources, validationId);
    505
    506            environment.push(ValidationTracker.class, activeTracker);
    507            environment.push(FormSupport.class, formSupport);
    508
    509            Heartbeat heartbeat = new HeartbeatImpl();
    510
    511            environment.push(Heartbeat.class, heartbeat);
    512
    513            heartbeat.begin();
    514
    515            boolean didPushBeanValidationContext = false;
    516
    517            try
    518            {
    519                resources.triggerContextEvent(EventConstants.PREPARE_FOR_SUBMIT, context, eventCallback);
    520
    521                if (eventCallback.isAborted())
    522                    return true;
    523
    524                resources.triggerContextEvent(EventConstants.PREPARE, context, eventCallback);
    525                if (eventCallback.isAborted())
    526                    return true;
    527
    528                if (isFormCancelled())
    529                {
    530                    resources.triggerContextEvent(EventConstants.CANCELED, context, eventCallback);
    531                    if (eventCallback.isAborted())
    532                        return true;
    533                }
    534
    535                environment.push(BeanValidationContext.class, new BeanValidationContextImpl(validate));
    536
    537                didPushBeanValidationContext = true;
    538
    539                executeStoredActions();
    540
    541                heartbeat.end();
    542
    543                formSupport.executeDeferred();
    544
    545                fireValidateEvent(EventConstants.VALIDATE, context, eventCallback);
    546
    547                if (eventCallback.isAborted())
    548                    return true;
    549
    550                // Let the listeners know about overall success or failure. Most
    551                // listeners fall into
    552                // one of those two camps.
    553
    554                // If the tracker has no errors, then clear it of any input values
    555                // as well, so that the next page render will be "clean" and show
    556                // true persistent data, not value from the previous form
    557                // submission.
    558
    559                if (!activeTracker.getHasErrors())
    560                    activeTracker.clear();
    561
    562                resources.triggerContextEvent(activeTracker.getHasErrors() ? EventConstants.FAILURE
    563                        : EventConstants.SUCCESS, context, eventCallback);
    564
    565                // Lastly, tell anyone whose interested that the form is completely
    566                // submitted.
    567
    568                if (eventCallback.isAborted())
    569                    return true;
    570
    571                resources.triggerContextEvent(EventConstants.SUBMIT, context, eventCallback);
    572
    573                return eventCallback.isAborted();
    574            } finally
    575            {
    576                environment.pop(Heartbeat.class);
    577                environment.pop(FormSupport.class);
    578
    579                environment.pop(ValidationTracker.class);
    580
    581                if (didPushBeanValidationContext)
    582                {
    583                    environment.pop(BeanValidationContext.class);
    584                }
    585
    586                activeTracker = null;
    587            }
    588        }
    589
    590        private boolean isFormCancelled()
    591        {
    592            // The "cancel" query parameter is reserved for this purpose; if it is present then the form was canceled on the
    593            // client side.  For image submits, there will be two parameters: "cancel.x" and "cancel.y".
    594
    595            if (request.getParameter(InternalConstants.CANCEL_NAME) != null ||
    596                    request.getParameter(InternalConstants.CANCEL_NAME + ".x") != null)
    597            {
    598                return true;
    599            }
    600
    601            // When JavaScript is involved, it's more complicated. In fact, this is part of HLS's desire
    602            // to have all forms submit via XHR when JavaScript is present, since it would provide
    603            // an opportunity to get the submitting element's value into the request properly.
    604
    605            String raw = request.getParameter(SUBMITTING_ELEMENT_ID);
    606
    607            if (InternalUtils.isNonBlank(raw) &&
    608                    new JSONArray(raw).getString(1).equals(InternalConstants.CANCEL_NAME))
    609            {
    610                return true;
    611            }
    612
    613            return false;
    614        }
    615
    616
    617        private void fireValidateEvent(String eventName, EventContext context, TrackableComponentEventCallback callback)
    618        {
    619            try
    620            {
    621                resources.triggerContextEvent(eventName, context, callback);
    622            } catch (RuntimeException ex)
    623            {
    624                ValidationException ve = ExceptionUtils.findCause(ex, ValidationException.class, propertyAccess);
    625
    626                if (ve != null)
    627                {
    628                    ValidationTracker tracker = environment.peek(ValidationTracker.class);
    629
    630                    tracker.recordError(ve.getMessage());
    631
    632                    return;
    633                }
    634
    635                throw ex;
    636            }
    637        }
    638
    639        /**
    640         * Pulls the stored actions out of the request, converts them from MIME
    641         * stream back to object stream and then
    642         * objects, and executes them.
    643         */
    644        private void executeStoredActions()
    645        {
    646            String[] values = request.getParameters(FORM_DATA);
    647
    648            if (!request.getMethod().equals("POST") || values == null)
    649                throw new RuntimeException(messages.format("invalid-request", FORM_DATA));
    650
    651            // Due to Ajax (FormInjector) there may be multiple values here, so
    652            // handle each one individually.
    653
    654            for (String clientEncodedActions : values)
    655            {
    656                if (InternalUtils.isBlank(clientEncodedActions))
    657                    continue;
    658
    659                logger.debug("Processing actions: {}", clientEncodedActions);
    660
    661                ObjectInputStream ois = null;
    662
    663                Component component = null;
    664
    665                try
    666                {
    667                    ois = clientDataEncoder.decodeClientData(clientEncodedActions);
    668
    669                    while (!eventCallback.isAborted())
    670                    {
    671                        String componentId = ois.readUTF();
    672                        ComponentAction action = (ComponentAction) ois.readObject();
    673
    674                        component = source.getComponent(componentId);
    675
    676                        logger.debug("Processing: {} {}", componentId, action);
    677
    678                        action.execute(component);
    679
    680                        component = null;
    681                    }
    682                } catch (EOFException ex)
    683                {
    684                    // Expected
    685                } catch (Exception ex)
    686                {
    687                    Location location = component == null ? null : component.getComponentResources().getLocation();
    688
    689                    throw new TapestryException(ex.getMessage(), location, ex);
    690                } finally
    691                {
    692                    InternalUtils.close(ois);
    693                }
    694            }
    695        }
    696
    697        public void recordError(String errorMessage)
    698        {
    699            getActiveTracker().recordError(errorMessage);
    700        }
    701
    702        public void recordError(Field field, String errorMessage)
    703        {
    704            getActiveTracker().recordError(field, errorMessage);
    705        }
    706
    707        public boolean getHasErrors()
    708        {
    709            return getActiveTracker().getHasErrors();
    710        }
    711
    712        public boolean isValid()
    713        {
    714            return !getActiveTracker().getHasErrors();
    715        }
    716
    717        private ValidationTracker getActiveTracker()
    718        {
    719            return activeTracker != null ? activeTracker : getWrappedTracker();
    720        }
    721
    722        public void clearErrors()
    723        {
    724            getActiveTracker().clear();
    725        }
    726
    727        // For testing:
    728
    729        void setTracker(ValidationTracker tracker)
    730        {
    731            this.tracker = tracker;
    732        }
    733
    734        /**
    735         * Forms use the same value for their name and their id attribute.
    736         */
    737        public String getClientId()
    738        {
    739            return clientId;
    740        }
    741
    742        @Inject
    743        private ComponentSource componentSource;
    744
    745        private void preallocateNames(IdAllocator idAllocator)
    746        {
    747            for (String name : TapestryInternalUtils.splitAtCommas(preselectedFormNames))
    748            {
    749                idAllocator.allocateId(name);
    750                // See https://issues.apache.org/jira/browse/TAP5-1632
    751                javascriptSupport.allocateClientId(name);
    752
    753            }
    754
    755            Component activePage = componentSource.getActivePage();
    756
    757            // This is unlikely but may be possible if people override some of the standard
    758            // exception reporting logic.
    759
    760            if (activePage == null)
    761                return;
    762
    763            ComponentResources activePageResources = activePage.getComponentResources();
    764
    765            try
    766            {
    767
    768                activePageResources.triggerEvent(EventConstants.PREALLOCATE_FORM_CONTROL_NAMES, new Object[]
    769                        {idAllocator}, null);
    770            } catch (RuntimeException ex)
    771            {
    772                logger.error(
    773                        String.format("Unable to obtrain form control names to preallocate: %s",
    774                                InternalUtils.toMessage(ex)), ex);
    775            }
    776        }
    777    }