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