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