001 // Copyright 2005 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 015 package org.apache.tapestry.form; 016 017 import org.apache.hivemind.ApplicationRuntimeException; 018 import org.apache.hivemind.HiveMind; 019 import org.apache.hivemind.Location; 020 import org.apache.hivemind.util.Defense; 021 import org.apache.tapestry.*; 022 import org.apache.tapestry.engine.ILink; 023 import org.apache.tapestry.event.BrowserEvent; 024 import org.apache.tapestry.json.JSONObject; 025 import org.apache.tapestry.services.ResponseBuilder; 026 import org.apache.tapestry.services.ServiceConstants; 027 import org.apache.tapestry.util.IdAllocator; 028 import org.apache.tapestry.valid.IValidationDelegate; 029 030 import java.util.*; 031 032 /** 033 * Encapsulates most of the behavior of a Form component. 034 * 035 */ 036 public class FormSupportImpl implements FormSupport 037 { 038 /** 039 * Name of query parameter storing the ids alloocated while rendering the form, as a comma 040 * seperated list. This information is used when the form is submitted, to ensure that the 041 * rewind allocates the exact same sequence of ids. 042 */ 043 044 public static final String FORM_IDS = "formids"; 045 046 /** 047 * Names of additional ids that were pre-reserved, as a comma-sepereated list. These are names 048 * beyond that standard set. Certain engine services include extra parameter values that must be 049 * accounted for, and page properties may be encoded as additional query parameters. 050 */ 051 052 public static final String RESERVED_FORM_IDS = "reservedids"; 053 054 /** 055 * Indicates why the form was submitted: whether for normal ("submit"), refresh, or because the 056 * form was canceled. 057 */ 058 059 public static final String SUBMIT_MODE = "submitmode"; 060 061 /** 062 * Attribute set to true when a field has been focused; used to prevent conflicting JavaScript 063 * for field focusing from being emitted. 064 */ 065 066 public static final String FIELD_FOCUS_ATTRIBUTE = "org.apache.tapestry.field-focused"; 067 068 private static final Set _standardReservedIds; 069 070 static 071 { 072 Set set = new HashSet(); 073 074 set.addAll(Arrays.asList(ServiceConstants.RESERVED_IDS)); 075 set.add(FORM_IDS); 076 set.add(RESERVED_FORM_IDS); 077 set.add(SUBMIT_MODE); 078 set.add(FormConstants.SUBMIT_NAME_PARAMETER); 079 080 _standardReservedIds = Collections.unmodifiableSet(set); 081 } 082 083 private static final Set _submitModes; 084 085 static 086 { 087 Set set = new HashSet(); 088 set.add(FormConstants.SUBMIT_CANCEL); 089 set.add(FormConstants.SUBMIT_NORMAL); 090 set.add(FormConstants.SUBMIT_REFRESH); 091 092 _submitModes = Collections.unmodifiableSet(set); 093 } 094 095 protected final IRequestCycle _cycle; 096 097 protected final IdAllocator _elementIdAllocator = new IdAllocator(); 098 099 /** 100 * Used when rewinding the form to figure to match allocated ids (allocated during the rewind) 101 * against expected ids (allocated in the previous request cycle, when the form was rendered). 102 */ 103 104 private int _allocatedIdIndex; 105 106 /** 107 * The list of allocated ids for form elements within this form. This list is constructed when a 108 * form renders, and is validated against when the form is rewound. 109 */ 110 111 private final List _allocatedIds = new ArrayList(); 112 113 private String _encodingType; 114 115 private final List _deferredRunnables = new ArrayList(); 116 117 /** 118 * Map keyed on extended component id, value is the pre-rendered markup for that component. 119 */ 120 121 private final Map _prerenderMap = new HashMap(); 122 123 /** 124 * {@link Map}, keyed on {@link FormEventType}. Values are either a String (the function name 125 * of a single event handler), or a List of Strings (a sequence of event handler function 126 * names). 127 */ 128 129 private Map _events; 130 131 private final IForm _form; 132 133 private final List _hiddenValues = new ArrayList(); 134 135 private final boolean _rewinding; 136 137 private final IMarkupWriter _writer; 138 139 private final IValidationDelegate _delegate; 140 141 private final PageRenderSupport _pageRenderSupport; 142 143 /** 144 * Client side validation is built up using a json object syntax structure 145 */ 146 private final JSONObject _profile; 147 148 /** 149 * Used to detect whether or not a form component has been updated and will require form sync on ajax requests 150 */ 151 private boolean _fieldUpdating; 152 153 public FormSupportImpl(IMarkupWriter writer, IRequestCycle cycle, IForm form) 154 { 155 Defense.notNull(writer, "writer"); 156 Defense.notNull(cycle, "cycle"); 157 Defense.notNull(form, "form"); 158 159 _writer = writer; 160 _cycle = cycle; 161 _form = form; 162 _delegate = form.getDelegate(); 163 164 _rewinding = cycle.isRewound(form); 165 _allocatedIdIndex = 0; 166 167 _pageRenderSupport = TapestryUtils.getOptionalPageRenderSupport(cycle); 168 _profile = new JSONObject(); 169 } 170 171 /** 172 * Alternate constructor used for testing only. 173 * 174 * @param cycle 175 * The current cycle. 176 */ 177 FormSupportImpl(IRequestCycle cycle) 178 { 179 _cycle = cycle; 180 _form = null; 181 _rewinding = false; 182 _writer = null; 183 _delegate = null; 184 _pageRenderSupport = null; 185 _profile = null; 186 } 187 188 /** 189 * {@inheritDoc} 190 */ 191 public IForm getForm() 192 { 193 return _form; 194 } 195 196 /** 197 * {@inheritDoc} 198 */ 199 public void addEventHandler(FormEventType type, String functionName) 200 { 201 if (_events == null) 202 _events = new HashMap(); 203 204 List functionList = (List) _events.get(type); 205 206 // The value can either be a String, or a List of String. Since 207 // it is rare for there to be more than one event handling function, 208 // we start with just a String. 209 210 if (functionList == null) 211 { 212 functionList = new ArrayList(); 213 214 _events.put(type, functionList); 215 } 216 217 functionList.add(functionName); 218 } 219 220 /** 221 * Adds hidden fields for parameters provided by the {@link ILink}. These parameters define the 222 * information needed to dispatch the request, plus state information. The names of these 223 * parameters must be reserved so that conflicts don't occur that could disrupt the request 224 * processing. For example, if the id 'page' is not reserved, then a conflict could occur with a 225 * component whose id is 'page'. A certain number of ids are always reserved, and we find any 226 * additional ids beyond that set. 227 */ 228 229 private void addHiddenFieldsForLinkParameters(ILink link) 230 { 231 String[] names = link.getParameterNames(); 232 int count = Tapestry.size(names); 233 234 StringBuffer extraIds = new StringBuffer(); 235 String sep = ""; 236 boolean hasExtra = false; 237 238 // All the reserved ids, which are essential for 239 // dispatching the request, are automatically reserved. 240 // Thus, if you have a component with an id of 'service', its element id 241 // will likely be 'service$0'. 242 243 preallocateReservedIds(); 244 245 for (int i = 0; i < count; i++) 246 { 247 String name = names[i]; 248 249 // Reserve the name. 250 251 if (!_standardReservedIds.contains(name)) 252 { 253 _elementIdAllocator.allocateId(name); 254 255 extraIds.append(sep); 256 extraIds.append(name); 257 258 sep = ","; 259 hasExtra = true; 260 } 261 262 addHiddenFieldsForLinkParameter(link, name); 263 } 264 265 if (hasExtra) 266 addHiddenValue(RESERVED_FORM_IDS, extraIds.toString()); 267 } 268 269 public void addHiddenValue(String name, String value) 270 { 271 _hiddenValues.add(new HiddenFieldData(name, value)); 272 } 273 274 public void addHiddenValue(String name, String id, String value) 275 { 276 _hiddenValues.add(new HiddenFieldData(name, id, value)); 277 } 278 279 /** 280 * Converts the allocateIds property into a string, a comma-separated list of ids. This is 281 * included as a hidden field in the form and is used to identify discrepencies when the form is 282 * submitted. 283 */ 284 285 private String buildAllocatedIdList() 286 { 287 StringBuffer buffer = new StringBuffer(); 288 int count = _allocatedIds.size(); 289 290 for (int i = 0; i < count; i++) 291 { 292 if (i > 0) 293 buffer.append(','); 294 295 buffer.append(_allocatedIds.get(i)); 296 } 297 298 return buffer.toString(); 299 } 300 301 private void emitEventHandlers(String formId) 302 { 303 if (_events == null || _events.isEmpty()) 304 return; 305 306 StringBuffer buffer = new StringBuffer(); 307 308 Iterator i = _events.entrySet().iterator(); 309 310 while (i.hasNext()) 311 { 312 Map.Entry entry = (Map.Entry) i.next(); 313 FormEventType type = (FormEventType) entry.getKey(); 314 Object value = entry.getValue(); 315 316 buffer.append("Tapestry."); 317 buffer.append(type.getAddHandlerFunctionName()); 318 buffer.append("('"); 319 buffer.append(formId); 320 buffer.append("', function (event)\n{"); 321 322 List l = (List) value; 323 int count = l.size(); 324 325 for (int j = 0; j < count; j++) 326 { 327 String functionName = (String) l.get(j); 328 329 if (j > 0) 330 { 331 buffer.append(";"); 332 } 333 334 buffer.append("\n "); 335 buffer.append(functionName); 336 337 // It's supposed to be function names, but some of Paul's validation code 338 // adds inline code to be executed instead. 339 340 if (!functionName.endsWith(")")) 341 { 342 buffer.append("()"); 343 } 344 } 345 346 buffer.append(";\n});\n"); 347 } 348 349 // TODO: If PRS is null ... 350 351 _pageRenderSupport.addInitializationScript(_form, buffer.toString()); 352 } 353 354 /** 355 * Constructs a unique identifier (within the Form). The identifier consists of the component's 356 * id, with an index number added to ensure uniqueness. 357 * <p> 358 * Simply invokes 359 * {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}with the 360 * component's id. 361 */ 362 363 public String getElementId(IFormComponent component) 364 { 365 return getElementId(component, component.getSpecifiedId()); 366 } 367 368 /** 369 * Constructs a unique identifier (within the Form). The identifier consists of the component's 370 * id, with an index number added to ensure uniqueness. 371 * <p> 372 * Simply invokes 373 * {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}with the 374 * component's id. 375 */ 376 377 public String getElementId(IFormComponent component, String baseId) 378 { 379 // $ is not a valid character in an XML/XHTML id, so convert it to an underscore. 380 381 String filteredId = TapestryUtils.convertTapestryIdToNMToken(baseId); 382 383 String result = _elementIdAllocator.allocateId(filteredId); 384 385 if (_rewinding) 386 { 387 if (_allocatedIdIndex >= _allocatedIds.size()) 388 { 389 throw new StaleLinkException(FormMessages.formTooManyIds(_form, _allocatedIds 390 .size(), component), component); 391 } 392 393 String expected = (String) _allocatedIds.get(_allocatedIdIndex); 394 395 if (!result.equals(expected)) 396 throw new StaleLinkException(FormMessages.formIdMismatch( 397 _form, 398 _allocatedIdIndex, 399 expected, 400 result, 401 component), component); 402 } 403 else 404 { 405 _allocatedIds.add(result); 406 } 407 408 _allocatedIdIndex++; 409 410 component.setName(result); 411 412 component.setClientId(result); 413 414 return result; 415 } 416 417 public String peekClientId(IFormComponent comp) 418 { 419 String id = comp.getSpecifiedId(); 420 if (id == null) 421 return null; 422 423 return _elementIdAllocator.peekNextId(id); 424 } 425 426 public boolean isRewinding() 427 { 428 return _rewinding; 429 } 430 431 private void preallocateReservedIds() 432 { 433 for (int i = 0; i < ServiceConstants.RESERVED_IDS.length; i++) 434 _elementIdAllocator.allocateId(ServiceConstants.RESERVED_IDS[i]); 435 } 436 437 /** 438 * Invoked when rewinding a form to re-initialize the _allocatedIds and _elementIdAllocator. 439 * Converts a string passed as a parameter (and containing a comma separated list of ids) back 440 * into the allocateIds property. In addition, return the state of the ID allocater back to 441 * where it was at the start of the render. 442 * 443 * @see #buildAllocatedIdList() 444 * @since 3.0 445 */ 446 447 private void reinitializeIdAllocatorForRewind() 448 { 449 String allocatedFormIds = _cycle.getParameter(FORM_IDS); 450 451 String[] ids = TapestryUtils.split(allocatedFormIds); 452 453 for (int i = 0; i < ids.length; i++) 454 _allocatedIds.add(ids[i]); 455 456 // Now, reconstruct the initial state of the 457 // id allocator. 458 459 preallocateReservedIds(); 460 461 String extraReservedIds = _cycle.getParameter(RESERVED_FORM_IDS); 462 463 ids = TapestryUtils.split(extraReservedIds); 464 465 for (int i = 0; i < ids.length; i++) 466 _elementIdAllocator.allocateId(ids[i]); 467 } 468 469 int convertSeedToId(String input) 470 { 471 int index = input.lastIndexOf("_"); 472 473 if (index < 0) 474 throw new ApplicationRuntimeException("Unable to convert seedId of " + input + " to integer."); 475 476 return Integer.parseInt(input.substring(index, input.length())); 477 } 478 479 public void render(String method, IRender informalParametersRenderer, ILink link, String scheme, Integer port) 480 { 481 String formId = _form.getName(); 482 483 emitEventManagerInitialization(formId); 484 485 // Convert the link's query parameters into a series of 486 // hidden field values (that will be rendered later). 487 488 addHiddenFieldsForLinkParameters(link); 489 490 // Create a hidden field to store the submission mode, in case 491 // client-side JavaScript forces an update. 492 493 addHiddenValue(SUBMIT_MODE, null); 494 495 // And another for the name of the component that 496 // triggered the submit. 497 498 addHiddenValue(FormConstants.SUBMIT_NAME_PARAMETER, null); 499 500 IMarkupWriter nested = _writer.getNestedWriter(); 501 502 _form.renderBody(nested, _cycle); 503 504 runDeferredRunnables(); 505 506 int portI = (port == null) ? 0 : port.intValue(); 507 508 writeTag(_writer, method, link.getURL(scheme, null, portI, null, false)); 509 510 // For XHTML compatibility 511 _writer.attribute("id", formId); 512 513 if (_encodingType != null) 514 _writer.attribute("enctype", _encodingType); 515 516 // Write out event handlers collected during the rendering. 517 518 emitEventHandlers(formId); 519 520 informalParametersRenderer.render(_writer, _cycle); 521 522 // Finish the <form> tag 523 524 _writer.println(); 525 526 writeHiddenFields(); 527 528 // Close the nested writer, inserting its contents. 529 530 nested.close(); 531 532 // Close the <form> tag. 533 534 _writer.end(); 535 536 String fieldId = _delegate.getFocusField(); 537 538 if (_pageRenderSupport == null) 539 return; 540 541 // If the form doesn't support focus, or the focus has already been set by a different form, 542 // then do nothing. 543 544 if (!_cycle.isFocusDisabled() && fieldId != null && _form.getFocus() 545 && _cycle.getAttribute(FIELD_FOCUS_ATTRIBUTE) == null) 546 { 547 _pageRenderSupport.addInitializationScript(_form, "dojo.require(\"tapestry.form\");tapestry.form.focusField('" + fieldId + "');"); 548 _cycle.setAttribute(FIELD_FOCUS_ATTRIBUTE, Boolean.TRUE); 549 } 550 551 // register the validation profile with client side form manager 552 553 if (_form.isClientValidationEnabled()) 554 { 555 IPage page = _form.getPage(); 556 557 // only include dojo widget layer if it's not already been included 558 559 if (!page.hasWidgets()) { 560 IAsset clientScript = _form.getAsset("clientValidationScript"); 561 if (clientScript != null){ 562 563 _pageRenderSupport.addExternalScript(_form, clientScript.getResourceLocation()); 564 } 565 } 566 567 _pageRenderSupport.addInitializationScript(_form, "dojo.require(\"tapestry.form\");tapestry.form.clearProfiles('" 568 + formId + "'); tapestry.form.registerProfile('" + formId + "'," 569 + _profile.toString() + ");"); 570 } 571 } 572 573 /** 574 * Pre-renders the form, setting up some client-side form support. Returns the name of the 575 * client-side form event manager variable. 576 * 577 * @param formId 578 * The client id of the form. 579 */ 580 protected void emitEventManagerInitialization(String formId) 581 { 582 if (_pageRenderSupport == null) 583 return; 584 585 StringBuffer str = new StringBuffer("dojo.require(\"tapestry.form\");"); 586 str.append("tapestry.form.registerForm(\"").append(formId).append("\""); 587 588 if (_form.isAsync()) { 589 590 str.append(", true"); 591 592 if (_form.isJson()) { 593 str.append(", true"); 594 } 595 } 596 597 str.append(");"); 598 599 _pageRenderSupport.addInitializationScript(_form, str.toString()); 600 } 601 602 public String rewind() 603 { 604 _form.getDelegate().clear(); 605 606 String mode = _cycle.getParameter(SUBMIT_MODE); 607 608 // On a cancel, don't bother rendering the body or anything else at all. 609 610 if (FormConstants.SUBMIT_CANCEL.equals(mode)) 611 return mode; 612 613 reinitializeIdAllocatorForRewind(); 614 615 _form.renderBody(_writer, _cycle); 616 617 // New, handles cases where an eventlistener 618 // causes a form submission. 619 620 BrowserEvent event = new BrowserEvent(_cycle); 621 622 _form.getEventInvoker().invokeFormListeners(this, _cycle, event); 623 624 int expected = _allocatedIds.size(); 625 626 // The other case, _allocatedIdIndex > expected, is 627 // checked for inside getElementId(). Remember that 628 // _allocatedIdIndex is incremented after allocating. 629 630 if (_allocatedIdIndex < expected) 631 { 632 String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex); 633 634 throw new StaleLinkException(FormMessages.formTooFewIds(_form, expected 635 - _allocatedIdIndex, nextExpectedId), _form); 636 } 637 638 runDeferredRunnables(); 639 640 if (_submitModes.contains(mode)) { 641 642 // clear errors during refresh 643 644 if (FormConstants.SUBMIT_REFRESH.equals(mode)) { 645 646 _form.getDelegate().clearErrors(); 647 } 648 649 return mode; 650 } 651 652 // Either something wacky on the client side, or a client without 653 // javascript enabled. 654 655 return FormConstants.SUBMIT_NORMAL; 656 } 657 658 private void runDeferredRunnables() 659 { 660 Iterator i = _deferredRunnables.iterator(); 661 while (i.hasNext()) 662 { 663 Runnable r = (Runnable) i.next(); 664 665 r.run(); 666 } 667 } 668 669 public void setEncodingType(String encodingType) 670 { 671 672 if (_encodingType != null && !_encodingType.equals(encodingType)) 673 throw new ApplicationRuntimeException(FormMessages.encodingTypeContention( 674 _form, 675 _encodingType, 676 encodingType), _form, null, null); 677 678 _encodingType = encodingType; 679 } 680 681 /** 682 * Overwridden by {@link org.apache.tapestry.wml.GoFormSupportImpl} (WML). 683 */ 684 protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value) 685 { 686 writer.beginEmpty("input"); 687 writer.attribute("type", "hidden"); 688 writer.attribute("name", name); 689 690 if (HiveMind.isNonBlank(id)) 691 writer.attribute("id", id); 692 693 writer.attribute("value", value == null ? "" : value); 694 writer.println(); 695 } 696 697 /** 698 * Writes out all hidden values previously added by 699 * {@link #addHiddenValue(String, String, String)}. Writes a <div> tag around 700 * {@link #writeHiddenFieldList(IMarkupWriter)}. Overriden by 701 * {@link org.apache.tapestry.wml.GoFormSupportImpl}. 702 */ 703 704 protected void writeHiddenFields() 705 { 706 IMarkupWriter writer = getHiddenFieldWriter(); 707 708 writer.begin("div"); 709 writer.attribute("style", "display:none;"); 710 writer.attribute("id", _form.getName() + "hidden"); 711 712 writeHiddenFieldList(writer); 713 714 writer.end(); 715 } 716 717 /** 718 * Writes out all hidden values previously added by 719 * {@link #addHiddenValue(String, String, String)}, plus the allocated id list. 720 */ 721 722 protected void writeHiddenFieldList(IMarkupWriter writer) 723 { 724 writeHiddenField(writer, FORM_IDS, null, buildAllocatedIdList()); 725 726 Iterator i = _hiddenValues.iterator(); 727 while (i.hasNext()) 728 { 729 HiddenFieldData data = (HiddenFieldData) i.next(); 730 731 writeHiddenField(writer, data.getName(), data.getId(), data.getValue()); 732 } 733 } 734 735 /** 736 * Determines if a hidden field change has occurred, which would require 737 * that we write hidden form fields using the {@link ResponseBuilder} 738 * writer. 739 * 740 * @return The default {@link IMarkupWriter} if not doing a managed ajax/json 741 * response, else whatever is returned from {@link ResponseBuilder}. 742 */ 743 protected IMarkupWriter getHiddenFieldWriter() 744 { 745 if (_cycle.getResponseBuilder().contains(_form) 746 || (!_fieldUpdating || !_cycle.getResponseBuilder().isDynamic()) ) { 747 return _writer; 748 } 749 750 return _cycle.getResponseBuilder().getWriter(_form.getName() + "hidden", 751 ResponseBuilder.ELEMENT_TYPE); 752 } 753 754 private void addHiddenFieldsForLinkParameter(ILink link, String parameterName) 755 { 756 String[] values = link.getParameterValues(parameterName); 757 758 // In some cases, there are no values, but a space is "reserved" for the provided name. 759 760 if (values == null) 761 return; 762 763 for (int i = 0; i < values.length; i++) 764 { 765 addHiddenValue(parameterName, values[i]); 766 } 767 } 768 769 protected void writeTag(IMarkupWriter writer, String method, String url) 770 { 771 writer.begin("form"); 772 writer.attribute("method", method); 773 writer.attribute("action", url); 774 } 775 776 public void prerenderField(IMarkupWriter writer, IComponent field, Location location) 777 { 778 Defense.notNull(writer, "writer"); 779 Defense.notNull(field, "field"); 780 781 String key = field.getExtendedId(); 782 783 if (_prerenderMap.containsKey(key)) 784 throw new ApplicationRuntimeException(FormMessages.fieldAlreadyPrerendered(field), field, location, null); 785 786 NestedMarkupWriter nested = writer.getNestedWriter(); 787 788 TapestryUtils.storePrerender(_cycle, field); 789 790 _cycle.getResponseBuilder().render(nested, field, _cycle); 791 792 TapestryUtils.removePrerender(_cycle); 793 794 _prerenderMap.put(key, nested.getBuffer()); 795 } 796 797 public boolean wasPrerendered(IMarkupWriter writer, IComponent field) 798 { 799 String key = field.getExtendedId(); 800 801 // During a rewind, if the form is pre-rendered, the buffer will be null, 802 // so do the check based on the key, not a non-null value. 803 804 if (!_prerenderMap.containsKey(key)) 805 return false; 806 807 String buffer = (String) _prerenderMap.get(key); 808 809 writer.printRaw(buffer); 810 811 _prerenderMap.remove(key); 812 813 return true; 814 } 815 816 public void addDeferredRunnable(Runnable runnable) 817 { 818 Defense.notNull(runnable, "runnable"); 819 820 _deferredRunnables.add(runnable); 821 } 822 823 public void registerForFocus(IFormComponent field, int priority) 824 { 825 _delegate.registerForFocus(field, priority); 826 } 827 828 /** 829 * {@inheritDoc} 830 */ 831 public JSONObject getProfile() 832 { 833 return _profile; 834 } 835 836 /** 837 * {@inheritDoc} 838 */ 839 public boolean isFormFieldUpdating() 840 { 841 return _fieldUpdating; 842 } 843 844 /** 845 * {@inheritDoc} 846 */ 847 public void setFormFieldUpdating(boolean value) 848 { 849 _fieldUpdating = value; 850 } 851 }