001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.corelib.components; 014 015import org.apache.tapestry5.*; 016import org.apache.tapestry5.annotations.*; 017import org.apache.tapestry5.corelib.ClientValidation; 018import org.apache.tapestry5.corelib.internal.ComponentActionSink; 019import org.apache.tapestry5.corelib.internal.FormSupportImpl; 020import org.apache.tapestry5.corelib.internal.InternalFormSupport; 021import org.apache.tapestry5.dom.Element; 022import org.apache.tapestry5.internal.BeanValidationContext; 023import org.apache.tapestry5.internal.BeanValidationContextImpl; 024import org.apache.tapestry5.internal.InternalConstants; 025import org.apache.tapestry5.internal.services.FormControlNameManager; 026import org.apache.tapestry5.internal.services.HeartbeatImpl; 027import org.apache.tapestry5.internal.util.AutofocusValidationDecorator; 028import org.apache.tapestry5.ioc.Location; 029import org.apache.tapestry5.ioc.Messages; 030import org.apache.tapestry5.ioc.annotations.Inject; 031import org.apache.tapestry5.ioc.annotations.Symbol; 032import org.apache.tapestry5.ioc.internal.util.InternalUtils; 033import org.apache.tapestry5.ioc.internal.util.TapestryException; 034import org.apache.tapestry5.ioc.services.PropertyAccess; 035import org.apache.tapestry5.ioc.util.ExceptionUtils; 036import org.apache.tapestry5.ioc.util.IdAllocator; 037import org.apache.tapestry5.json.JSONArray; 038import org.apache.tapestry5.runtime.Component; 039import org.apache.tapestry5.services.*; 040import org.apache.tapestry5.services.compatibility.DeprecationWarning; 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 * 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. 057 * 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. 061 * 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. 064 * 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 * 069 * 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. 074 * 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 * 079 * 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. It also places a {@link ValidationTracker} into the environment during both render and submission. 082 * During submission it also pushes a {@link Heartbeat} into the environment, which is {@link org.apache.tapestry5.services.Heartbeat#end() ended} just before 083 * {@linkplain FormSupport#defer(Runnable) deferred FormSupport operations} are executed. 084 * 085 * 086 * @tapestrydoc 087 * @see BeanEditForm 088 * @see Errors 089 * @see FormFragment 090 * @see Label 091 */ 092@Events( 093 {EventConstants.PREPARE_FOR_RENDER, EventConstants.PREPARE, EventConstants.PREPARE_FOR_SUBMIT, 094 EventConstants.VALIDATE, EventConstants.SUBMIT, EventConstants.FAILURE, EventConstants.SUCCESS, EventConstants.CANCELED}) 095@SupportsInformalParameters 096public class Form implements ClientElement, FormValidationControl 097{ 098 /** 099 * Query parameter name storing form data (the serialized commands needed to 100 * process a form submission). 101 */ 102 public static final String FORM_DATA = "t:formdata"; 103 104 /** 105 * Used by {@link Submit}, etc., to identify which particular client-side element (by element id) 106 * was responsible for the submission. An empty hidden field is created, as needed, to store this value. 107 * Starting in Tapestry 5.3, this is a JSONArray with two values: the client id followed by the client name. 108 * 109 * @since 5.2.0 110 */ 111 public static final String SUBMITTING_ELEMENT_ID = "t:submit"; 112 113 public static final StreamPageContent STREAM_ACTIVE_PAGE_CONTENT = new StreamPageContent().withoutActivation(); 114 115 /** 116 * The context for the link (optional parameter). This list of values will 117 * be converted into strings and included in 118 * the URI. The strings will be coerced back to whatever their values are 119 * and made available to event handler 120 * methods. 121 */ 122 @Parameter 123 private Object[] context; 124 125 /** 126 * The object which will record user input and validation errors. When not using 127 * the default behavior supplied by the Form component (an immediate re-render of the active 128 * page when there are form validation errors), it is necessary to bind this parameter 129 * to a persistent value that can be maintained until the active page is re-rendered. See 130 * <a href="https://issues.apache.org/jira/browse/TAP5-1808">TAP5-1801</a>. 131 */ 132 @Parameter("defaultTracker") 133 protected ValidationTracker tracker; 134 135 @Inject 136 @Symbol(SymbolConstants.FORM_CLIENT_LOGIC_ENABLED) 137 private boolean clientLogicDefaultEnabled; 138 139 /** 140 * Controls when client validation occurs on the client, if at all. Defaults to {@link ClientValidation#SUBMIT}. 141 * {@link ClientValidation#BLUR} was the default, prior to Tapestry 5.4, but is no longer supported. 142 */ 143 @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL) 144 private ClientValidation clientValidation = clientLogicDefaultEnabled ? ClientValidation.SUBMIT 145 : ClientValidation.NONE; 146 147 /** 148 * If true (the default), then the JavaScript will be added to position the 149 * cursor into the form. The field to 150 * receive focus is the first rendered field that is in error, or required, 151 * or present (in that order of priority). 152 * 153 * @see SymbolConstants#FORM_CLIENT_LOGIC_ENABLED 154 */ 155 @Parameter 156 private boolean autofocus = clientLogicDefaultEnabled; 157 158 /** 159 * Binding the zone parameter will cause the form submission to be handled 160 * as an Ajax request that updates the 161 * indicated zone. Often a Form will update the same zone that contains it. 162 */ 163 @Parameter(defaultPrefix = BindingConstants.LITERAL) 164 private String zone; 165 166 /** 167 * If true, then the Form's action will be secure (using an absolute URL with the HTTPs scheme) regardless 168 * of whether the containing page itself is secure or not. This parameter does nothing 169 * when {@linkplain SymbolConstants#SECURE_ENABLED security is disabled} (which is often 170 * the case in development mode). This only affects how the Form's action attribute is rendered, there is 171 * not (currently) a check that the form is actually submitted securely. 172 */ 173 @Parameter 174 private boolean secure; 175 176 /** 177 * Prefix value used when searching for validation messages and constraints. 178 * The default is the Form component's 179 * id. This is overridden by {@link org.apache.tapestry5.corelib.components.BeanEditForm}. 180 * 181 * @see org.apache.tapestry5.services.FormSupport#getFormValidationId() 182 */ 183 @Parameter 184 private String validationId; 185 186 /** 187 * Object to validate during the form submission process. The default is the Form component's container. 188 * This parameter should only be used in combination with the Bean Validation Library. 189 */ 190 @Parameter 191 private Object validate; 192 193 /** 194 * When true, the the form will submit as an asynchronous request (via XmlHttpRequest); the event handler methods 195 * can make use of the {@link org.apache.tapestry5.services.ajax.AjaxResponseRenderer} in order to force content 196 * updates to the client. This is used as an alternative to placing the form inside a {@link org.apache.tapestry5.corelib.components.Zone} 197 * and binding the {@code zone} parameter. 198 * 199 * @since 5.4 200 */ 201 @Parameter 202 private boolean async = false; 203 204 @Inject 205 private Logger logger; 206 207 @Inject 208 private Environment environment; 209 210 @Inject 211 private ComponentResources resources; 212 213 @Inject 214 private Messages messages; 215 216 @Environmental 217 private JavaScriptSupport javascriptSupport; 218 219 @Inject 220 private Request request; 221 222 @Inject 223 private ComponentSource source; 224 225 @Inject 226 private FormControlNameManager formControlNameManager; 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 } catch (UnsupportedEncodingException e) 394 { 395 logger.error(String.format( 396 "Enable to decode parameter value for parameter %s in form %s", 397 parameterName, form.getName()), e); 398 } 399 writer.element("input", "type", "hidden", "name", parameterName, "value", value); 400 writer.end(); 401 } 402 } 403 404 writer.end(); // div 405 406 environment.peek(Heartbeat.class).begin(); 407 } 408 409 /** 410 * Creates an {@link org.apache.tapestry5.corelib.internal.InternalFormSupport} for 411 * this Form. 412 * 413 * This method may also be invoked as the handler for the "internalCreateRenderTimeFormSupport" event. 414 * 415 * @param clientId 416 * the client-side id for the rendered form 417 * element 418 * @param actionSink 419 * used to collect component actions that will, ultimately, be 420 * written as the t:formdata hidden 421 * field 422 * @param allocator 423 * used to allocate unique ids 424 * @return form support object 425 */ 426 @OnEvent("internalCreateRenderTimeFormSupport") 427 InternalFormSupport createRenderTimeFormSupport(String clientId, ComponentActionSink actionSink, 428 IdAllocator allocator) 429 { 430 return new FormSupportImpl(resources, clientId, actionSink, 431 clientValidation != ClientValidation.NONE, allocator, validationId); 432 } 433 434 void afterRender(MarkupWriter writer) 435 { 436 environment.peek(Heartbeat.class).end(); 437 438 formSupport.executeDeferred(); 439 440 String encodingType = formSupport.getEncodingType(); 441 442 if (encodingType != null) 443 { 444 form.forceAttributes("enctype", encodingType); 445 } 446 447 writer.end(); // form 448 449 div.element("input", "type", "hidden", "name", FORM_DATA, "value", actionSink.getClientData()); 450 div.pop(); 451 452 if (autofocus) 453 { 454 environment.pop(ValidationDecorator.class); 455 } 456 } 457 458 void cleanupRender() 459 { 460 environment.pop(FormSupport.class); 461 462 formSupport = null; 463 464 environment.pop(ValidationTracker.class); 465 466 tracker.clear(); 467 468 environment.pop(BeanValidationContext.class); 469 } 470 471 @SuppressWarnings( 472 {"unchecked", "InfiniteLoopStatement"}) 473 Object onAction(EventContext context) throws IOException 474 { 475 beforeProcessSubmit(context); 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 { 526 return true; 527 } 528 529 afterValidate(); 530 531 // Let the listeners know about overall success or failure. Most 532 // listeners fall into 533 // one of those two camps. 534 535 // If the tracker has no errors, then clear it of any input values 536 // as well, so that the next page render will be "clean" and show 537 // true persistent data, not value from the previous form 538 // submission. 539 540 if (!tracker.getHasErrors()) 541 { 542 tracker.clear(); 543 } 544 545 String eventType = tracker.getHasErrors() 546 ? EventConstants.FAILURE 547 : EventConstants.SUCCESS; 548 549 resources.triggerContextEvent(eventType, context, eventCallback); 550 551 if (eventCallback.isAborted()) 552 { 553 return true; 554 } 555 556 // Lastly, tell anyone whose interested that the form is completely 557 // submitted. 558 559 resources.triggerContextEvent(EventConstants.SUBMIT, context, eventCallback); 560 561 afterSuccessOrFailure(); 562 563 if (eventCallback.isAborted()) 564 { 565 return true; 566 } 567 568 // For traditional request with no validation exceptions, re-render the 569 // current page immediately, as-is. Prior to Tapestry 5.4, a redirect was 570 // sent that required that the tracker be persisted across requests. 571 // See https://issues.apache.org/jira/browse/TAP5-1808 572 573 if (tracker.getHasErrors() && !request.isXHR()) 574 { 575 return STREAM_ACTIVE_PAGE_CONTENT; 576 } 577 578 // The event will not work its way up. 579 580 return false; 581 582 } finally 583 { 584 environment.pop(Heartbeat.class); 585 environment.pop(FormSupport.class); 586 587 environment.pop(ValidationTracker.class); 588 589 if (didPushBeanValidationContext) 590 { 591 environment.pop(BeanValidationContext.class); 592 } 593 } 594 } 595 596 /** 597 * A hook invoked from {@link #onAction(org.apache.tapestry5.EventContext)} after the 598 * {@link org.apache.tapestry5.EventConstants#SUBMIT} or {@link org.apache.tapestry5.EventConstants#FAILURE} event has been triggered. 599 * 600 * This method will be invoked regardless of whether the submit or failure event was aborted. 601 * 602 * This implementation does nothing. 603 * 604 * @since 5.4 605 */ 606 607 protected void afterSuccessOrFailure() 608 { 609 610 } 611 612 /** 613 * A hook invoked from {@link #onAction(org.apache.tapestry5.EventContext)} before any other setup. 614 * 615 * This implementation does nothing. 616 * 617 * @param context 618 * as passed to {@code onAction()} 619 * @since 5.4 620 */ 621 protected void beforeProcessSubmit(EventContext context) 622 { 623 624 } 625 626 /** 627 * A hook invoked from {@link #onAction(org.apache.tapestry5.EventContext)} after the 628 * {@link org.apache.tapestry5.EventConstants#VALIDATE} event has been triggered, and 629 * before the {@link #tracker} has been {@linkplain org.apache.tapestry5.ValidationTracker#clear() cleared}. 630 * 631 * Only invoked if the valiate event did not abort (that is, the no event handler method returned a value). 632 * 633 * This implementation does nothing. 634 * 635 * @since 5.4 636 */ 637 protected void afterValidate() 638 { 639 640 } 641 642 private boolean isFormCancelled() 643 { 644 // The "cancel" query parameter is reserved for this purpose; if it is present then the form was canceled on the 645 // client side. For image submits, there will be two parameters: "cancel.x" and "cancel.y". 646 647 if (request.getParameter(InternalConstants.CANCEL_NAME) != null || 648 request.getParameter(InternalConstants.CANCEL_NAME + ".x") != null) 649 { 650 return true; 651 } 652 653 // When JavaScript is involved, it's more complicated. In fact, this is part of HLS's desire 654 // to have all forms submit via XHR when JavaScript is present, since it would provide 655 // an opportunity to get the submitting element's value into the request properly. 656 657 String raw = request.getParameter(SUBMITTING_ELEMENT_ID); 658 659 if (InternalUtils.isNonBlank(raw) && 660 new JSONArray(raw).getString(1).equals(InternalConstants.CANCEL_NAME)) 661 { 662 return true; 663 } 664 665 return false; 666 } 667 668 669 private void fireValidateEvent(String eventName, EventContext context, TrackableComponentEventCallback callback) 670 { 671 try 672 { 673 resources.triggerContextEvent(eventName, context, callback); 674 } catch (RuntimeException ex) 675 { 676 ValidationException ve = ExceptionUtils.findCause(ex, ValidationException.class, propertyAccess); 677 678 if (ve != null) 679 { 680 ValidationTracker tracker = environment.peek(ValidationTracker.class); 681 682 tracker.recordError(ve.getMessage()); 683 684 return; 685 } 686 687 throw ex; 688 } 689 } 690 691 /** 692 * Pulls the stored actions out of the request, converts them from MIME 693 * stream back to object stream and then 694 * objects, and executes them. 695 */ 696 private void executeStoredActions(boolean forFormCancel) 697 { 698 String[] values = request.getParameters(FORM_DATA); 699 700 if (!request.getMethod().equals("POST") || values == null) 701 throw new RuntimeException(messages.format("core-invalid-form-request", FORM_DATA)); 702 703 // Due to Ajax there may be multiple values here, so 704 // handle each one individually. 705 706 for (String clientEncodedActions : values) 707 { 708 if (InternalUtils.isBlank(clientEncodedActions)) 709 continue; 710 711 logger.debug("Processing actions: {}", clientEncodedActions); 712 713 ObjectInputStream ois = null; 714 715 Component component = null; 716 717 try 718 { 719 ois = clientDataEncoder.decodeClientData(clientEncodedActions); 720 721 while (!eventCallback.isAborted()) 722 { 723 String componentId = ois.readUTF(); 724 boolean cancelAction = ois.readBoolean(); 725 ComponentAction action = (ComponentAction) ois.readObject(); 726 727 // Actions are a mix of ordinary actions and cancel actions. Filter out one set or the other 728 // based on whether the form was submitted or cancelled. 729 if (forFormCancel != cancelAction) 730 { 731 continue; 732 } 733 734 component = source.getComponent(componentId); 735 736 logger.debug("Processing: {} {}", componentId, action); 737 738 action.execute(component); 739 740 component = null; 741 } 742 } catch (EOFException ex) 743 { 744 // Expected 745 } catch (Exception ex) 746 { 747 Location location = component == null ? null : component.getComponentResources().getLocation(); 748 749 throw new TapestryException(ex.getMessage(), location, ex); 750 } finally 751 { 752 InternalUtils.close(ois); 753 } 754 } 755 } 756 757 public void recordError(String errorMessage) 758 { 759 tracker.recordError(errorMessage); 760 } 761 762 public void recordError(Field field, String errorMessage) 763 { 764 tracker.recordError(field, errorMessage); 765 } 766 767 public boolean getHasErrors() 768 { 769 return tracker.getHasErrors(); 770 } 771 772 public boolean isValid() 773 { 774 return !tracker.getHasErrors(); 775 } 776 777 public void clearErrors() 778 { 779 tracker.clear(); 780 } 781 782 // For testing: 783 784 void setTracker(ValidationTracker tracker) 785 { 786 this.tracker = tracker; 787 } 788 789 /** 790 * Forms use the same value for their name and their id attribute. 791 */ 792 public String getClientId() 793 { 794 return clientId; 795 } 796 797 private void preallocateNames(IdAllocator idAllocator) 798 { 799 for (String name : formControlNameManager.getReservedNames()) 800 { 801 idAllocator.allocateId(name); 802 // See https://issues.apache.org/jira/browse/TAP5-1632 803 javascriptSupport.allocateClientId(name); 804 805 } 806 807 Component activePage = componentSource.getActivePage(); 808 809 // This is unlikely but may be possible if people override some of the standard 810 // exception reporting logic. 811 812 if (activePage == null) 813 return; 814 815 ComponentResources activePageResources = activePage.getComponentResources(); 816 817 try 818 { 819 820 activePageResources.triggerEvent(EventConstants.PREALLOCATE_FORM_CONTROL_NAMES, new Object[] 821 {idAllocator}, null); 822 } catch (RuntimeException ex) 823 { 824 logger.error( 825 String.format("Unable to obtain form control names to preallocate: %s", 826 ExceptionUtils.toMessage(ex)), ex); 827 } 828 } 829}