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
015package org.apache.tapestry5.corelib.components;
016
017import org.apache.tapestry5.*;
018import org.apache.tapestry5.annotations.*;
019import org.apache.tapestry5.corelib.ClientValidation;
020import org.apache.tapestry5.corelib.internal.ComponentActionSink;
021import org.apache.tapestry5.corelib.internal.FormSupportImpl;
022import org.apache.tapestry5.corelib.internal.InternalFormSupport;
023import org.apache.tapestry5.dom.Element;
024import org.apache.tapestry5.internal.*;
025import org.apache.tapestry5.internal.services.HeartbeatImpl;
026import org.apache.tapestry5.internal.util.AutofocusValidationDecorator;
027import org.apache.tapestry5.ioc.Location;
028import org.apache.tapestry5.ioc.Messages;
029import org.apache.tapestry5.ioc.annotations.Inject;
030import org.apache.tapestry5.ioc.annotations.Symbol;
031import org.apache.tapestry5.ioc.internal.util.InternalUtils;
032import org.apache.tapestry5.ioc.internal.util.TapestryException;
033import org.apache.tapestry5.ioc.services.PropertyAccess;
034import org.apache.tapestry5.ioc.util.ExceptionUtils;
035import org.apache.tapestry5.ioc.util.IdAllocator;
036import org.apache.tapestry5.json.JSONArray;
037import org.apache.tapestry5.json.JSONObject;
038import org.apache.tapestry5.runtime.Component;
039import org.apache.tapestry5.services.*;
040import org.apache.tapestry5.services.javascript.InitializationPriority;
041import org.apache.tapestry5.services.javascript.JavaScriptSupport;
042import org.slf4j.Logger;
043
044import java.io.EOFException;
045import java.io.IOException;
046import java.io.ObjectInputStream;
047import java.io.UnsupportedEncodingException;
048import java.net.URLDecoder;
049
050/**
051 * An HTML form, which will enclose other components to render out the various
052 * types of fields.
053 * <p>
054 * A Form triggers many notification events. When it renders, it triggers a
055 * {@link org.apache.tapestry5.EventConstants#PREPARE_FOR_RENDER} notification, followed by a
056 * {@link EventConstants#PREPARE} notification.</p>
057 * <p>
058 * When the form is submitted, the component triggers several notifications: first a
059 * {@link EventConstants#PREPARE_FOR_SUBMIT}, then a {@link EventConstants#PREPARE}: these allow the page to update its
060 * state as necessary to prepare for the form submission.</p>
061 * <p>
062 * The Form component then determines if the form was cancelled (see {@link org.apache.tapestry5.corelib.SubmitMode#CANCEL}). If so,
063 * a {@link EventConstants#CANCELED} event is triggered.</p>
064 * <p>
065 * Next come notifications to contained components (or more accurately, the execution of stored {@link ComponentAction}s), to allow each component to retrieve and validate
066 * submitted values, and update server-side properties.  This is based on the {@code t:formdata} query parameter,
067 * which contains serialized object data (generated when the form initially renders).
068 * </p>
069 * <p>Once the form data is processed, the next step is to trigger the
070 * {@link EventConstants#VALIDATE}, which allows for cross-form validation. After that, either a
071 * {@link EventConstants#SUCCESS} OR {@link EventConstants#FAILURE} event (depending on whether the
072 * {@link ValidationTracker} has recorded any errors). Lastly, a {@link EventConstants#SUBMIT} event, for any listeners
073 * that care only about form submission, regardless of success or failure.</p>
074 * <p>
075 * For all of these notifications, the event context is derived from the <strong>context</strong> component parameter. This
076 * context is encoded into the form's action URI (the parameter is not read when the form is submitted, instead the
077 * values encoded into the form are used).
078 * </p>
079 * <p>
080 * While rendering, or processing a Form submission, the Form component places a {@link FormSupport} object into the {@linkplain Environment environment},
081 * so that enclosed components can coordinate with the Form component.
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
094public 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}