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    }