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