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