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