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 * Adds an event handler for the form, of the given type. 186 */ 187 188 public void addEventHandler(FormEventType type, String functionName) 189 { 190 if (_events == null) 191 _events = new HashMap(); 192 193 List functionList = (List) _events.get(type); 194 195 // The value can either be a String, or a List of String. Since 196 // it is rare for there to be more than one event handling function, 197 // we start with just a String. 198 199 if (functionList == null) 200 { 201 functionList = new ArrayList(); 202 203 _events.put(type, functionList); 204 } 205 206 functionList.add(functionName); 207 } 208 209 /** 210 * Adds hidden fields for parameters provided by the {@link ILink}. These parameters define the 211 * information needed to dispatch the request, plus state information. The names of these 212 * parameters must be reserved so that conflicts don't occur that could disrupt the request 213 * processing. For example, if the id 'page' is not reserved, then a conflict could occur with a 214 * component whose id is 'page'. A certain number of ids are always reserved, and we find any 215 * additional ids beyond that set. 216 */ 217 218 private void addHiddenFieldsForLinkParameters(ILink link) 219 { 220 String[] names = link.getParameterNames(); 221 int count = Tapestry.size(names); 222 223 StringBuffer extraIds = new StringBuffer(); 224 String sep = ""; 225 boolean hasExtra = false; 226 227 // All the reserved ids, which are essential for 228 // dispatching the request, are automatically reserved. 229 // Thus, if you have a component with an id of 'service', its element id 230 // will likely be 'service$0'. 231 232 preallocateReservedIds(); 233 234 for (int i = 0; i < count; i++) 235 { 236 String name = names[i]; 237 238 // Reserve the name. 239 240 if (!_standardReservedIds.contains(name)) 241 { 242 _elementIdAllocator.allocateId(name); 243 244 extraIds.append(sep); 245 extraIds.append(name); 246 247 sep = ","; 248 hasExtra = true; 249 } 250 251 addHiddenFieldsForLinkParameter(link, name); 252 } 253 254 if (hasExtra) 255 addHiddenValue(RESERVED_FORM_IDS, extraIds.toString()); 256 } 257 258 public void addHiddenValue(String name, String value) 259 { 260 _hiddenValues.add(new HiddenFieldData(name, value)); 261 } 262 263 public void addHiddenValue(String name, String id, String value) 264 { 265 _hiddenValues.add(new HiddenFieldData(name, id, value)); 266 } 267 268 /** 269 * Converts the allocateIds property into a string, a comma-separated list of ids. This is 270 * included as a hidden field in the form and is used to identify discrepencies when the form is 271 * submitted. 272 */ 273 274 private String buildAllocatedIdList() 275 { 276 StringBuffer buffer = new StringBuffer(); 277 int count = _allocatedIds.size(); 278 279 for (int i = 0; i < count; i++) 280 { 281 if (i > 0) 282 buffer.append(','); 283 284 buffer.append(_allocatedIds.get(i)); 285 } 286 287 return buffer.toString(); 288 } 289 290 private void emitEventHandlers(String formId) 291 { 292 if (_events == null || _events.isEmpty()) 293 return; 294 295 StringBuffer buffer = new StringBuffer(); 296 297 Iterator i = _events.entrySet().iterator(); 298 299 while (i.hasNext()) 300 { 301 Map.Entry entry = (Map.Entry) i.next(); 302 FormEventType type = (FormEventType) entry.getKey(); 303 Object value = entry.getValue(); 304 305 buffer.append("Tapestry."); 306 buffer.append(type.getAddHandlerFunctionName()); 307 buffer.append("('"); 308 buffer.append(formId); 309 buffer.append("', function (event)\n{"); 310 311 List l = (List) value; 312 int count = l.size(); 313 314 for (int j = 0; j < count; j++) 315 { 316 String functionName = (String) l.get(j); 317 318 if (j > 0) 319 { 320 buffer.append(";"); 321 } 322 323 buffer.append("\n "); 324 buffer.append(functionName); 325 326 // It's supposed to be function names, but some of Paul's validation code 327 // adds inline code to be executed instead. 328 329 if (!functionName.endsWith(")")) 330 { 331 buffer.append("()"); 332 } 333 } 334 335 buffer.append(";\n});\n"); 336 } 337 338 // TODO: If PRS is null ... 339 340 _pageRenderSupport.addInitializationScript(buffer.toString()); 341 } 342 343 /** 344 * Constructs a unique identifier (within the Form). The identifier consists of the component's 345 * id, with an index number added to ensure uniqueness. 346 * <p> 347 * Simply invokes 348 * {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}with the 349 * component's id. 350 */ 351 352 public String getElementId(IFormComponent component) 353 { 354 return getElementId(component, component.getId()); 355 } 356 357 /** 358 * Constructs a unique identifier (within the Form). The identifier consists of the component's 359 * id, with an index number added to ensure uniqueness. 360 * <p> 361 * Simply invokes 362 * {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}with the 363 * component's id. 364 */ 365 366 public String getElementId(IFormComponent component, String baseId) 367 { 368 // $ is not a valid character in an XML/XHTML id, so convert it to an underscore. 369 370 String filteredId = TapestryUtils.convertTapestryIdToNMToken(baseId); 371 372 String result = _elementIdAllocator.allocateId(filteredId); 373 374 if (_rewinding) 375 { 376 if (_allocatedIdIndex >= _allocatedIds.size()) 377 { 378 throw new StaleLinkException(FormMessages.formTooManyIds(_form, _allocatedIds 379 .size(), component), component); 380 } 381 382 String expected = (String) _allocatedIds.get(_allocatedIdIndex); 383 384 if (!result.equals(expected)) 385 throw new StaleLinkException(FormMessages.formIdMismatch( 386 _form, 387 _allocatedIdIndex, 388 expected, 389 result, 390 component), component); 391 } 392 else 393 { 394 _allocatedIds.add(result); 395 } 396 397 _allocatedIdIndex++; 398 399 component.setName(result); 400 401 return result; 402 } 403 404 public boolean isRewinding() 405 { 406 return _rewinding; 407 } 408 409 private void preallocateReservedIds() 410 { 411 for (int i = 0; i < ServiceConstants.RESERVED_IDS.length; i++) 412 _elementIdAllocator.allocateId(ServiceConstants.RESERVED_IDS[i]); 413 } 414 415 /** 416 * Invoked when rewinding a form to re-initialize the _allocatedIds and _elementIdAllocator. 417 * Converts a string passed as a parameter (and containing a comma separated list of ids) back 418 * into the allocateIds property. In addition, return the state of the ID allocater back to 419 * where it was at the start of the render. 420 * 421 * @see #buildAllocatedIdList() 422 * @since 3.0 423 */ 424 425 private void reinitializeIdAllocatorForRewind() 426 { 427 String allocatedFormIds = _cycle.getParameter(FORM_IDS); 428 429 String[] ids = TapestryUtils.split(allocatedFormIds); 430 431 for (int i = 0; i < ids.length; i++) 432 _allocatedIds.add(ids[i]); 433 434 // Now, reconstruct the the initial state of the 435 // id allocator. 436 437 preallocateReservedIds(); 438 439 String extraReservedIds = _cycle.getParameter(RESERVED_FORM_IDS); 440 441 ids = TapestryUtils.split(extraReservedIds); 442 443 for (int i = 0; i < ids.length; i++) 444 _elementIdAllocator.allocateId(ids[i]); 445 } 446 447 public void render(String method, IRender informalParametersRenderer, ILink link, String scheme) 448 { 449 String formId = _form.getName(); 450 451 emitEventManagerInitialization(formId); 452 453 // Convert the link's query parameters into a series of 454 // hidden field values (that will be rendered later). 455 456 addHiddenFieldsForLinkParameters(link); 457 458 // Create a hidden field to store the submission mode, in case 459 // client-side JavaScript forces an update. 460 461 addHiddenValue(SUBMIT_MODE, null); 462 463 // And another for the name of the component that 464 // triggered the submit. 465 466 addHiddenValue(FormConstants.SUBMIT_NAME_PARAMETER, null); 467 468 IMarkupWriter nested = _writer.getNestedWriter(); 469 470 _form.renderBody(nested, _cycle); 471 472 runDeferredRunnables(); 473 474 writeTag(_writer, method, link.getURL(scheme, null, 0, null, false)); 475 476 // For HTML compatibility 477 _writer.attribute("name", formId); 478 479 // For XHTML compatibility 480 _writer.attribute("id", formId); 481 482 if (_encodingType != null) 483 _writer.attribute("enctype", _encodingType); 484 485 // Write out event handlers collected during the rendering. 486 487 emitEventHandlers(formId); 488 489 informalParametersRenderer.render(_writer, _cycle); 490 491 // Finish the <form> tag 492 493 _writer.println(); 494 495 writeHiddenFields(); 496 497 // Close the nested writer, inserting its contents. 498 499 nested.close(); 500 501 // Close the <form> tag. 502 503 _writer.end(); 504 505 String fieldId = _delegate.getFocusField(); 506 507 if (fieldId == null || _pageRenderSupport == null) 508 return; 509 510 // If the form doesn't support focus, or the focus has already been set by a different form, 511 // then do nothing. 512 513 if (!_form.getFocus() || _cycle.getAttribute(FIELD_FOCUS_ATTRIBUTE) != null) 514 return; 515 516 _pageRenderSupport.addInitializationScript("Tapestry.set_focus('" + fieldId + "');"); 517 518 _cycle.setAttribute(FIELD_FOCUS_ATTRIBUTE, Boolean.TRUE); 519 } 520 521 /** 522 * Pre-renders the form, setting up some client-side form support. Returns the name of the 523 * client-side form event manager variable. 524 */ 525 protected void emitEventManagerInitialization(String formId) 526 { 527 if (_pageRenderSupport == null) 528 return; 529 530 _pageRenderSupport.addExternalScript(_script); 531 532 _pageRenderSupport.addInitializationScript("Tapestry.register_form('" + formId + "');"); 533 } 534 535 public String rewind() 536 { 537 _form.getDelegate().clear(); 538 539 String mode = _cycle.getParameter(SUBMIT_MODE); 540 541 // On a cancel, don't bother rendering the body or anything else at all. 542 543 if (FormConstants.SUBMIT_CANCEL.equals(mode)) 544 return mode; 545 546 reinitializeIdAllocatorForRewind(); 547 548 _form.renderBody(_writer, _cycle); 549 550 int expected = _allocatedIds.size(); 551 552 // The other case, _allocatedIdIndex > expected, is 553 // checked for inside getElementId(). Remember that 554 // _allocatedIdIndex is incremented after allocating. 555 556 if (_allocatedIdIndex < expected) 557 { 558 String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex); 559 560 throw new StaleLinkException(FormMessages.formTooFewIds(_form, expected 561 - _allocatedIdIndex, nextExpectedId), _form); 562 } 563 564 runDeferredRunnables(); 565 566 if (_submitModes.contains(mode)) 567 return mode; 568 569 // Either something wacky on the client side, or a client without 570 // javascript enabled. 571 572 return FormConstants.SUBMIT_NORMAL; 573 574 } 575 576 private void runDeferredRunnables() 577 { 578 Iterator i = _deferredRunnables.iterator(); 579 while (i.hasNext()) 580 { 581 Runnable r = (Runnable) i.next(); 582 583 r.run(); 584 } 585 } 586 587 public void setEncodingType(String encodingType) 588 { 589 590 if (_encodingType != null && !_encodingType.equals(encodingType)) 591 throw new ApplicationRuntimeException(FormMessages.encodingTypeContention( 592 _form, 593 _encodingType, 594 encodingType), _form, null, null); 595 596 _encodingType = encodingType; 597 } 598 599 /** 600 * Overwridden by {@link org.apache.tapestry.wml.GoFormSupportImpl} (WML). 601 */ 602 protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value) 603 { 604 writer.beginEmpty("input"); 605 writer.attribute("type", "hidden"); 606 writer.attribute("name", name); 607 608 if (HiveMind.isNonBlank(id)) 609 writer.attribute("id", id); 610 611 writer.attribute("value", value == null ? "" : value); 612 writer.println(); 613 } 614 615 private void writeHiddenField(String name, String id, String value) 616 { 617 writeHiddenField(_writer, name, id, value); 618 } 619 620 /** 621 * Writes out all hidden values previously added by 622 * {@link #addHiddenValue(String, String, String)}. Writes a <div> tag around 623 * {@link #writeHiddenFieldList()}. Overriden by 624 * {@link org.apache.tapestry.wml.GoFormSupportImpl}. 625 */ 626 627 protected void writeHiddenFields() 628 { 629 _writer.begin("div"); 630 _writer.attribute("style", "display:none;"); 631 632 writeHiddenFieldList(); 633 634 _writer.end(); 635 } 636 637 /** 638 * Writes out all hidden values previously added by 639 * {@link #addHiddenValue(String, String, String)}, plus the allocated id list. 640 */ 641 642 protected void writeHiddenFieldList() 643 { 644 writeHiddenField(FORM_IDS, null, buildAllocatedIdList()); 645 646 Iterator i = _hiddenValues.iterator(); 647 while (i.hasNext()) 648 { 649 HiddenFieldData data = (HiddenFieldData) i.next(); 650 651 writeHiddenField(data.getName(), data.getId(), data.getValue()); 652 } 653 } 654 655 private void addHiddenFieldsForLinkParameter(ILink link, String parameterName) 656 { 657 String[] values = link.getParameterValues(parameterName); 658 659 // In some cases, there are no values, but a space is "reserved" for the provided name. 660 661 if (values == null) 662 return; 663 664 for (int i = 0; i < values.length; i++) 665 { 666 addHiddenValue(parameterName, values[i]); 667 } 668 } 669 670 protected void writeTag(IMarkupWriter writer, String method, String url) 671 { 672 writer.begin("form"); 673 writer.attribute("method", method); 674 writer.attribute("action", url); 675 } 676 677 public void prerenderField(IMarkupWriter writer, IComponent field, Location location) 678 { 679 Defense.notNull(writer, "writer"); 680 Defense.notNull(field, "field"); 681 682 String key = field.getExtendedId(); 683 684 if (_prerenderMap.containsKey(key)) 685 throw new ApplicationRuntimeException(FormMessages.fieldAlreadyPrerendered(field), 686 location, null); 687 688 NestedMarkupWriter nested = writer.getNestedWriter(); 689 690 field.render(nested, _cycle); 691 692 _prerenderMap.put(key, nested.getBuffer()); 693 } 694 695 public boolean wasPrerendered(IMarkupWriter writer, IComponent field) 696 { 697 String key = field.getExtendedId(); 698 699 // During a rewind, if the form is pre-rendered, the buffer will be null, 700 // so do the check based on the key, not a non-null value. 701 702 if (!_prerenderMap.containsKey(key)) 703 return false; 704 705 String buffer = (String) _prerenderMap.get(key); 706 707 writer.printRaw(buffer); 708 709 _prerenderMap.remove(key); 710 711 return true; 712 } 713 714 public void addDeferredRunnable(Runnable runnable) 715 { 716 Defense.notNull(runnable, "runnable"); 717 718 _deferredRunnables.add(runnable); 719 } 720 721 public void registerForFocus(IFormComponent field, int priority) 722 { 723 _delegate.registerForFocus(field, priority); 724 } 725 726 }