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