001// Copyright 2006-2014 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.runtime.Component; 038import org.apache.tapestry5.services.*; 039import org.apache.tapestry5.services.compatibility.DeprecationWarning; 040import org.apache.tapestry5.services.javascript.JavaScriptSupport; 041import org.slf4j.Logger; 042 043import java.io.EOFException; 044import java.io.IOException; 045import java.io.ObjectInputStream; 046import java.io.UnsupportedEncodingException; 047import java.net.URLDecoder; 048 049/** 050 * An HTML form, which will enclose other components to render out the various 051 * types of fields. 052 * <p> 053 * A Form triggers many notification events. When it renders, it triggers a 054 * {@link org.apache.tapestry5.EventConstants#PREPARE_FOR_RENDER} notification, followed by a 055 * {@link EventConstants#PREPARE} notification.</p> 056 * <p> 057 * When the form is submitted, the component triggers several notifications: first a 058 * {@link EventConstants#PREPARE_FOR_SUBMIT}, then a {@link EventConstants#PREPARE}: these allow the page to update its 059 * state as necessary to prepare for the form submission.</p> 060 * <p> 061 * The Form component then determines if the form was cancelled (see {@link org.apache.tapestry5.corelib.SubmitMode#CANCEL}). If so, 062 * a {@link EventConstants#CANCELED} event is triggered.</p> 063 * <p> 064 * Next come notifications to contained components (or more accurately, the execution of stored {@link ComponentAction}s), to allow each component to retrieve and validate 065 * submitted values, and update server-side properties. This is based on the {@code t:formdata} query parameter, 066 * which contains serialized object data (generated when the form initially renders). 067 * </p> 068 * <p>Once the form data is processed, the next step is to trigger the 069 * {@link EventConstants#VALIDATE}, which allows for cross-form validation. After that, either a 070 * {@link EventConstants#SUCCESS} OR {@link EventConstants#FAILURE} event (depending on whether the 071 * {@link ValidationTracker} has recorded any errors). Lastly, a {@link EventConstants#SUBMIT} event, for any listeners 072 * that care only about form submission, regardless of success or failure.</p> 073 * <p> 074 * For all of these notifications, the event context is derived from the <strong>context</strong> component parameter. This 075 * context is encoded into the form's action URI (the parameter is not read when the form is submitted, instead the 076 * values encoded into the form are used). 077 * </p> 078 * <p> 079 * While rendering, or processing a Form submission, the Form component places a {@link FormSupport} object into the {@linkplain Environment environment}, 080 * so that enclosed components can coordinate with the Form component. It also places a {@link ValidationTracker} into the environment during both render and submission. 081 * During submission it also pushes a {@link Heartbeat} into the environment, which is {@link org.apache.tapestry5.services.Heartbeat#end() ended} just before 082 * {@linkplain FormSupport#defer(Runnable) deferred FormSupport operations} are executed. 083 * </p> 084 * 085 * @tapestrydoc 086 * @see BeanEditForm 087 * @see Errors 088 * @see FormFragment 089 * @see Label 090 */ 091@Events( 092 {EventConstants.PREPARE_FOR_RENDER, EventConstants.PREPARE, EventConstants.PREPARE_FOR_SUBMIT, 093 EventConstants.VALIDATE, EventConstants.SUBMIT, EventConstants.FAILURE, EventConstants.SUCCESS, EventConstants.CANCELED}) 094@SupportsInformalParameters 095public class Form implements ClientElement, FormValidationControl 096{ 097 /** 098 * Query parameter name storing form data (the serialized commands needed to 099 * process a form submission). 100 */ 101 public static final String FORM_DATA = "t:formdata"; 102 103 /** 104 * Used by {@link Submit}, etc., to identify which particular client-side element (by element id) 105 * was responsible for the submission. An empty hidden field is created, as needed, to store this value. 106 * Starting in Tapestry 5.3, this is a JSONArray with two values: the client id followed by the client name. 107 * 108 * @since 5.2.0 109 */ 110 public static final String SUBMITTING_ELEMENT_ID = "t:submit"; 111 112 public static final StreamPageContent STREAM_ACTIVE_PAGE_CONTENT = new StreamPageContent().withoutActivation(); 113 114 /** 115 * The context for the link (optional parameter). This list of values will 116 * be converted into strings and included in 117 * the URI. The strings will be coerced back to whatever their values are 118 * and made available to event handler 119 * methods. 120 */ 121 @Parameter 122 private Object[] context; 123 124 /** 125 * The object which will record user input and validation errors. When not using 126 * the default behavior supplied by the Form component (an immediate re-render of the active 127 * page when there are form validation errors), it is necessary to bind this parameter 128 * to a persistent value that can be maintained until the active page is re-rendered. See 129 * <a href="https://issues.apache.org/jira/browse/TAP5-1808">TAP5-1801</a>. 130 */ 131 @Parameter("defaultTracker") 132 private ValidationTracker tracker; 133 134 @Inject 135 @Symbol(SymbolConstants.FORM_CLIENT_LOGIC_ENABLED) 136 private boolean clientLogicDefaultEnabled; 137 138 /** 139 * Controls when client validation occurs on the client, if at all. Defaults to {@link ClientValidation#SUBMIT}. 140 * {@link ClientValidation#BLUR} was the default, prior to Tapestry 5.4, but is no longer supported. 141 */ 142 @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL) 143 private ClientValidation clientValidation = clientLogicDefaultEnabled ? ClientValidation.SUBMIT 144 : ClientValidation.NONE; 145 146 /** 147 * If true (the default), then the JavaScript will be added to position the 148 * cursor into the form. The field to 149 * receive focus is the first rendered field that is in error, or required, 150 * or present (in that order of priority). 151 * 152 * @see SymbolConstants#FORM_CLIENT_LOGIC_ENABLED 153 */ 154 @Parameter 155 private boolean autofocus = clientLogicDefaultEnabled; 156 157 /** 158 * Binding the zone parameter will cause the form submission to be handled 159 * as an Ajax request that updates the 160 * indicated zone. Often a Form will update the same zone that contains it. 161 */ 162 @Parameter(defaultPrefix = BindingConstants.LITERAL) 163 private String zone; 164 165 /** 166 * If true, then the Form's action will be secure (using an absolute URL with the HTTPs scheme) regardless 167 * of whether the containing page itself is secure or not. This parameter does nothing 168 * when {@linkplain SymbolConstants#SECURE_ENABLED security is disabled} (which is often 169 * the case in development mode). This only affects how the Form's action attribute is rendered, there is 170 * not (currently) a check that the form is actually submitted securely. 171 */ 172 @Parameter 173 private boolean secure; 174 175 /** 176 * Prefix value used when searching for validation messages and constraints. 177 * The default is the Form component's 178 * id. This is overridden by {@link org.apache.tapestry5.corelib.components.BeanEditForm}. 179 * 180 * @see org.apache.tapestry5.services.FormSupport#getFormValidationId() 181 */ 182 @Parameter 183 private String validationId; 184 185 /** 186 * Object to validate during the form submission process. The default is the Form component's container. 187 * This parameter should only be used in combination with the Bean Validation Library. 188 */ 189 @Parameter 190 private Object validate; 191 192 /** 193 * When true, the the form will submit as an asynchronous request (via XmlHttpRequest); the event handler methods 194 * can make use of the {@link org.apache.tapestry5.services.ajax.AjaxResponseRenderer} in order to force content 195 * updates to the client. This is used as an alternative to placing the form inside a {@link org.apache.tapestry5.corelib.components.Zone} 196 * and binding the {@code zone} parameter. 197 * 198 * @since 5.4 199 */ 200 @Parameter 201 private boolean async = false; 202 203 @Inject 204 private Logger logger; 205 206 @Inject 207 private Environment environment; 208 209 @Inject 210 private ComponentResources resources; 211 212 @Inject 213 private Messages messages; 214 215 @Environmental 216 private JavaScriptSupport javascriptSupport; 217 218 @Inject 219 private Request request; 220 221 @Inject 222 private ComponentSource source; 223 224 @Inject 225 @Symbol(InternalSymbols.PRE_SELECTED_FORM_NAMES) 226 private String preselectedFormNames; 227 228 229 /** 230 * Starting in 5.4, this is a simple, non-persistent property, with no extra magic tricks. 231 */ 232 private ValidationTracker defaultTracker; 233 234 @Inject 235 @Symbol(SymbolConstants.SECURE_ENABLED) 236 private boolean secureEnabled; 237 238 private InternalFormSupport formSupport; 239 240 private Element form; 241 242 private Element div; 243 244 // Collects a stream of component actions. Each action goes in as a UTF 245 // string (the component 246 // component id), followed by a ComponentAction 247 248 private ComponentActionSink actionSink; 249 250 @SuppressWarnings("unchecked") 251 @Environmental 252 private TrackableComponentEventCallback eventCallback; 253 254 @Inject 255 private ClientDataEncoder clientDataEncoder; 256 257 @Inject 258 private PropertyAccess propertyAccess; 259 260 @Inject 261 private DeprecationWarning deprecationWarning; 262 263 private String clientId; 264 265 @Inject 266 private ComponentSource componentSource; 267 268 String defaultValidationId() 269 { 270 return resources.getId(); 271 } 272 273 Object defaultValidate() 274 { 275 return resources.getContainer(); 276 } 277 278 /** 279 * Returns an instance of {@link ValidationTrackerImpl}, lazily creating it as needed. This property 280 * is the default for the <strong>tracker</strong> parameter; the property (as of Tapestry 5.4) is not 281 * persistent. 282 * 283 * @return per-request cached instance 284 */ 285 public ValidationTracker getDefaultTracker() 286 { 287 if (defaultTracker == null) 288 { 289 defaultTracker = new ValidationTrackerImpl(); 290 } 291 292 return defaultTracker; 293 } 294 295 /** 296 * @deprecated In 5.4; previously used only for testing 297 */ 298 public void setDefaultTracker(ValidationTracker defaultTracker) 299 { 300 this.defaultTracker = defaultTracker; 301 } 302 303 void setupRender() 304 { 305 FormSupport existing = environment.peek(FormSupport.class); 306 307 if (existing != null) 308 { 309 throw new TapestryException(messages.get("core-form-nesting-not-allowed"), existing, null); 310 } 311 312 if (clientValidation == ClientValidation.BLUR) 313 { 314 deprecationWarning.componentParameterValue(resources, "clientValidation", clientValidation, "BLUR is no longer supported, starting in 5.4. Validation will occur as with SUBMIT."); 315 } 316 } 317 318 void beginRender(MarkupWriter writer) 319 { 320 Link link = resources.createFormEventLink(EventConstants.ACTION, context); 321 322 String actionURL = secure && secureEnabled ? link.toAbsoluteURI(true) : link.toURI(); 323 324 actionSink = new ComponentActionSink(logger, clientDataEncoder); 325 326 clientId = javascriptSupport.allocateClientId(resources); 327 328 // Pre-register some names, to prevent client-side collisions with function names 329 // attached to the JS Form object. 330 331 IdAllocator allocator = new IdAllocator(); 332 333 preallocateNames(allocator); 334 335 formSupport = createRenderTimeFormSupport(clientId, actionSink, allocator); 336 337 environment.push(FormSupport.class, formSupport); 338 environment.push(ValidationTracker.class, tracker); 339 340 if (autofocus) 341 { 342 ValidationDecorator autofocusDecorator = new AutofocusValidationDecorator( 343 environment.peek(ValidationDecorator.class), tracker, javascriptSupport); 344 environment.push(ValidationDecorator.class, autofocusDecorator); 345 } 346 347 // Now that the environment is setup, inform the component or other 348 // listeners that the form 349 // is about to render. 350 351 resources.triggerEvent(EventConstants.PREPARE_FOR_RENDER, context, null); 352 353 resources.triggerEvent(EventConstants.PREPARE, context, null); 354 355 // Push BeanValidationContext only after the container had a chance to prepare 356 environment.push(BeanValidationContext.class, new BeanValidationContextImpl(validate)); 357 358 // Save the form element for later, in case we want to write an encoding 359 // type attribute. 360 361 form = writer.element("form", 362 "id", clientId, 363 "method", "post", 364 "action", actionURL, 365 "data-update-zone", zone); 366 367 if (clientValidation != ClientValidation.NONE) 368 { 369 writer.attributes("data-validate", "submit"); 370 } 371 372 if (async) 373 { 374 javascriptSupport.require("t5/core/zone"); 375 writer.attributes("data-async-trigger", true); 376 } 377 378 resources.renderInformalParameters(writer); 379 380 div = writer.element("div"); 381 382 for (String parameterName : link.getParameterNames()) 383 { 384 String[] values = link.getParameterValues(parameterName); 385 386 for (String value : values) 387 { 388 // The parameter value is expected to be encoded, 389 // but the input value shouldn't be encoded. 390 try 391 { 392 value = URLDecoder.decode(value, "UTF-8"); 393 } 394 catch (UnsupportedEncodingException e) 395 { 396 logger.error(String.format( 397 "Enable to decode parameter value for parameter %s in form %s", 398 parameterName, form.getName()), e); 399 } 400 writer.element("input", "type", "hidden", "name", parameterName, "value", value); 401 writer.end(); 402 } 403 } 404 405 writer.end(); // div 406 407 environment.peek(Heartbeat.class).begin(); 408 } 409 410 /** 411 * Creates an {@link org.apache.tapestry5.corelib.internal.InternalFormSupport} for 412 * this Form. This method is used 413 * by {@link org.apache.tapestry5.corelib.components.FormInjector}. 414 * <p/> 415 * This method may also be invoked as the handler for the "internalCreateRenderTimeFormSupport" event. 416 * 417 * @param clientId 418 * the client-side id for the rendered form 419 * element 420 * @param actionSink 421 * used to collect component actions that will, ultimately, be 422 * written as the t:formdata hidden 423 * field 424 * @param allocator 425 * used to allocate unique ids 426 * @return form support object 427 */ 428 @OnEvent("internalCreateRenderTimeFormSupport") 429 InternalFormSupport createRenderTimeFormSupport(String clientId, ComponentActionSink actionSink, 430 IdAllocator allocator) 431 { 432 return new FormSupportImpl(resources, clientId, actionSink, 433 clientValidation != ClientValidation.NONE, allocator, validationId); 434 } 435 436 void afterRender(MarkupWriter writer) 437 { 438 environment.peek(Heartbeat.class).end(); 439 440 formSupport.executeDeferred(); 441 442 String encodingType = formSupport.getEncodingType(); 443 444 if (encodingType != null) 445 { 446 form.forceAttributes("enctype", encodingType); 447 } 448 449 writer.end(); // form 450 451 div.element("input", "type", "hidden", "name", FORM_DATA, "value", actionSink.getClientData()); 452 div.pop(); 453 454 if (autofocus) 455 { 456 environment.pop(ValidationDecorator.class); 457 } 458 } 459 460 void cleanupRender() 461 { 462 environment.pop(FormSupport.class); 463 464 formSupport = null; 465 466 environment.pop(ValidationTracker.class); 467 468 tracker.clear(); 469 470 environment.pop(BeanValidationContext.class); 471 } 472 473 @SuppressWarnings( 474 {"unchecked", "InfiniteLoopStatement"}) 475 Object onAction(EventContext context) throws IOException 476 { 477 tracker.clear(); 478 479 formSupport = new FormSupportImpl(resources, validationId); 480 481 environment.push(ValidationTracker.class, tracker); 482 environment.push(FormSupport.class, formSupport); 483 484 Heartbeat heartbeat = new HeartbeatImpl(); 485 486 environment.push(Heartbeat.class, heartbeat); 487 488 heartbeat.begin(); 489 490 boolean didPushBeanValidationContext = false; 491 492 try 493 { 494 resources.triggerContextEvent(EventConstants.PREPARE_FOR_SUBMIT, context, eventCallback); 495 496 if (eventCallback.isAborted()) 497 return true; 498 499 resources.triggerContextEvent(EventConstants.PREPARE, context, eventCallback); 500 if (eventCallback.isAborted()) 501 return true; 502 503 if (isFormCancelled()) 504 { 505 executeStoredActions(true); 506 507 resources.triggerContextEvent(EventConstants.CANCELED, context, eventCallback); 508 if (eventCallback.isAborted()) 509 return true; 510 } 511 512 environment.push(BeanValidationContext.class, new BeanValidationContextImpl(validate)); 513 514 didPushBeanValidationContext = true; 515 516 executeStoredActions(false); 517 518 heartbeat.end(); 519 520 formSupport.executeDeferred(); 521 522 fireValidateEvent(EventConstants.VALIDATE, context, eventCallback); 523 524 if (eventCallback.isAborted()) 525 return true; 526 527 // Let the listeners know about overall success or failure. Most 528 // listeners fall into 529 // one of those two camps. 530 531 // If the tracker has no errors, then clear it of any input values 532 // as well, so that the next page render will be "clean" and show 533 // true persistent data, not value from the previous form 534 // submission. 535 536 if (!tracker.getHasErrors()) 537 { 538 tracker.clear(); 539 } 540 541 String eventType = tracker.getHasErrors() 542 ? EventConstants.FAILURE 543 : EventConstants.SUCCESS; 544 545 resources.triggerContextEvent(eventType, context, eventCallback); 546 547 if (eventCallback.isAborted()) 548 { 549 return true; 550 } 551 552 // Lastly, tell anyone whose interested that the form is completely 553 // submitted. 554 555 resources.triggerContextEvent(EventConstants.SUBMIT, context, eventCallback); 556 557 if (eventCallback.isAborted()) 558 { 559 return true; 560 } 561 562 // For traditional request with no validation exceptions, re-render the 563 // current page immediately, as-is. Prior to Tapestry 5.4, a redirect was 564 // sent that required that the tracker be persisted across requests. 565 // See https://issues.apache.org/jira/browse/TAP5-1808 566 567 if (tracker.getHasErrors() && !request.isXHR()) 568 { 569 return STREAM_ACTIVE_PAGE_CONTENT; 570 } 571 572 // The event will not work its way up. 573 574 return false; 575 576 } finally 577 { 578 environment.pop(Heartbeat.class); 579 environment.pop(FormSupport.class); 580 581 environment.pop(ValidationTracker.class); 582 583 if (didPushBeanValidationContext) 584 { 585 environment.pop(BeanValidationContext.class); 586 } 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(boolean forFormCancel) 645 { 646 String[] values = request.getParameters(FORM_DATA); 647 648 if (!request.getMethod().equals("POST") || values == null) 649 throw new RuntimeException(messages.format("core-invalid-form-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 boolean cancelAction = ois.readBoolean(); 673 ComponentAction action = (ComponentAction) ois.readObject(); 674 675 // Actions are a mix of ordinary actions and cancel actions. Filter out one set or the other 676 // based on whether the form was submitted or cancelled. 677 if (forFormCancel != cancelAction) { continue; } 678 679 component = source.getComponent(componentId); 680 681 logger.debug("Processing: {} {}", componentId, action); 682 683 action.execute(component); 684 685 component = null; 686 } 687 } catch (EOFException ex) 688 { 689 // Expected 690 } catch (Exception ex) 691 { 692 Location location = component == null ? null : component.getComponentResources().getLocation(); 693 694 throw new TapestryException(ex.getMessage(), location, ex); 695 } finally 696 { 697 InternalUtils.close(ois); 698 } 699 } 700 } 701 702 public void recordError(String errorMessage) 703 { 704 tracker.recordError(errorMessage); 705 } 706 707 public void recordError(Field field, String errorMessage) 708 { 709 tracker.recordError(field, errorMessage); 710 } 711 712 public boolean getHasErrors() 713 { 714 return tracker.getHasErrors(); 715 } 716 717 public boolean isValid() 718 { 719 return !tracker.getHasErrors(); 720 } 721 722 public void clearErrors() 723 { 724 tracker.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 private void preallocateNames(IdAllocator idAllocator) 743 { 744 for (String name : TapestryInternalUtils.splitAtCommas(preselectedFormNames)) 745 { 746 idAllocator.allocateId(name); 747 // See https://issues.apache.org/jira/browse/TAP5-1632 748 javascriptSupport.allocateClientId(name); 749 750 } 751 752 Component activePage = componentSource.getActivePage(); 753 754 // This is unlikely but may be possible if people override some of the standard 755 // exception reporting logic. 756 757 if (activePage == null) 758 return; 759 760 ComponentResources activePageResources = activePage.getComponentResources(); 761 762 try 763 { 764 765 activePageResources.triggerEvent(EventConstants.PREALLOCATE_FORM_CONTROL_NAMES, new Object[] 766 {idAllocator}, null); 767 } catch (RuntimeException ex) 768 { 769 logger.error( 770 String.format("Unable to obtrain form control names to preallocate: %s", 771 ExceptionUtils.toMessage(ex)), ex); 772 } 773 } 774}