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 }