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