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 }