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 015package org.apache.tapestry.form; 016 017import java.util.ArrayList; 018import java.util.Arrays; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026 027import org.apache.hivemind.ApplicationRuntimeException; 028import org.apache.hivemind.HiveMind; 029import org.apache.hivemind.Location; 030import org.apache.hivemind.Resource; 031import org.apache.hivemind.util.ClasspathResource; 032import org.apache.hivemind.util.Defense; 033import org.apache.tapestry.IComponent; 034import org.apache.tapestry.IForm; 035import org.apache.tapestry.IMarkupWriter; 036import org.apache.tapestry.IRender; 037import org.apache.tapestry.IRequestCycle; 038import org.apache.tapestry.NestedMarkupWriter; 039import org.apache.tapestry.PageRenderSupport; 040import org.apache.tapestry.StaleLinkException; 041import org.apache.tapestry.Tapestry; 042import org.apache.tapestry.TapestryUtils; 043import org.apache.tapestry.engine.ILink; 044import org.apache.tapestry.services.ServiceConstants; 045import org.apache.tapestry.util.IdAllocator; 046import 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 */ 054public 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 /** 464 * @deprecated Please use second render method. 465 */ 466 public void render(String method, IRender informalParametersRenderer, ILink link, String scheme) 467 { 468 render(method, informalParametersRenderer, link, scheme, null); 469 } 470 public void render(String method, IRender informalParametersRenderer, ILink link, 471 String scheme, Integer port) 472 { 473 String formId = _form.getName(); 474 475 emitEventManagerInitialization(formId); 476 477 // Convert the link's query parameters into a series of 478 // hidden field values (that will be rendered later). 479 480 addHiddenFieldsForLinkParameters(link); 481 482 // Create a hidden field to store the submission mode, in case 483 // client-side JavaScript forces an update. 484 485 addHiddenValue(SUBMIT_MODE, null); 486 487 // And another for the name of the component that 488 // triggered the submit. 489 490 addHiddenValue(FormConstants.SUBMIT_NAME_PARAMETER, null); 491 492 IMarkupWriter nested = _writer.getNestedWriter(); 493 494 _form.renderBody(nested, _cycle); 495 496 runDeferredRunnables(); 497 498 int portI = (port == null) ? 0 : port.intValue(); 499 writeTag(_writer, method, link.getURL(scheme, null, portI, null, false)); 500 501 // For HTML compatibility 502 _writer.attribute("name", formId); 503 504 // For XHTML compatibility 505 _writer.attribute("id", formId); 506 507 if (_encodingType != null) 508 _writer.attribute("enctype", _encodingType); 509 510 // Write out event handlers collected during the rendering. 511 512 emitEventHandlers(formId); 513 514 informalParametersRenderer.render(_writer, _cycle); 515 516 // Finish the <form> tag 517 518 _writer.println(); 519 520 writeHiddenFields(); 521 522 // Close the nested writer, inserting its contents. 523 524 nested.close(); 525 526 // Close the <form> tag. 527 528 _writer.end(); 529 530 String fieldId = _delegate.getFocusField(); 531 532 if (fieldId == null || _pageRenderSupport == null) 533 return; 534 535 // If the form doesn't support focus, or the focus has already been set by a different form, 536 // then do nothing. 537 538 if (!_form.getFocus() || _cycle.getAttribute(FIELD_FOCUS_ATTRIBUTE) != null) 539 return; 540 541 _pageRenderSupport.addInitializationScript("Tapestry.set_focus('" + fieldId + "');"); 542 543 _cycle.setAttribute(FIELD_FOCUS_ATTRIBUTE, Boolean.TRUE); 544 } 545 546 /** 547 * Pre-renders the form, setting up some client-side form support. Returns the name of the 548 * client-side form event manager variable. 549 */ 550 protected void emitEventManagerInitialization(String formId) 551 { 552 if (_pageRenderSupport == null) 553 return; 554 555 _pageRenderSupport.addExternalScript(_script); 556 557 _pageRenderSupport.addInitializationScript("Tapestry.register_form('" + formId + "');"); 558 } 559 560 public String rewind() 561 { 562 _form.getDelegate().clear(); 563 564 String mode = _cycle.getParameter(SUBMIT_MODE); 565 566 // On a cancel, don't bother rendering the body or anything else at all. 567 568 if (FormConstants.SUBMIT_CANCEL.equals(mode)) 569 return mode; 570 571 reinitializeIdAllocatorForRewind(); 572 573 _form.renderBody(_writer, _cycle); 574 575 int expected = _allocatedIds.size(); 576 577 // The other case, _allocatedIdIndex > expected, is 578 // checked for inside getElementId(). Remember that 579 // _allocatedIdIndex is incremented after allocating. 580 581 if (_allocatedIdIndex < expected) 582 { 583 String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex); 584 585 throw new StaleLinkException(FormMessages.formTooFewIds(_form, expected 586 - _allocatedIdIndex, nextExpectedId), _form); 587 } 588 589 runDeferredRunnables(); 590 591 if (_submitModes.contains(mode)) 592 return mode; 593 594 // Either something wacky on the client side, or a client without 595 // javascript enabled. 596 597 return FormConstants.SUBMIT_NORMAL; 598 599 } 600 601 private void runDeferredRunnables() 602 { 603 Iterator i = _deferredRunnables.iterator(); 604 while (i.hasNext()) 605 { 606 Runnable r = (Runnable) i.next(); 607 608 r.run(); 609 } 610 } 611 612 public void setEncodingType(String encodingType) 613 { 614 615 if (_encodingType != null && !_encodingType.equals(encodingType)) 616 throw new ApplicationRuntimeException(FormMessages.encodingTypeContention( 617 _form, 618 _encodingType, 619 encodingType), _form, null, null); 620 621 _encodingType = encodingType; 622 } 623 624 /** 625 * Overwridden by {@link org.apache.tapestry.wml.GoFormSupportImpl} (WML). 626 */ 627 protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value) 628 { 629 writer.beginEmpty("input"); 630 writer.attribute("type", "hidden"); 631 writer.attribute("name", name); 632 633 if (HiveMind.isNonBlank(id)) 634 writer.attribute("id", id); 635 636 writer.attribute("value", value == null ? "" : value); 637 writer.println(); 638 } 639 640 private void writeHiddenField(String name, String id, String value) 641 { 642 writeHiddenField(_writer, name, id, value); 643 } 644 645 /** 646 * Writes out all hidden values previously added by 647 * {@link #addHiddenValue(String, String, String)}. Writes a <div> tag around 648 * {@link #writeHiddenFieldList()}. Overriden by 649 * {@link org.apache.tapestry.wml.GoFormSupportImpl}. 650 */ 651 652 protected void writeHiddenFields() 653 { 654 _writer.begin("div"); 655 _writer.attribute("style", "display:none;"); 656 657 writeHiddenFieldList(); 658 659 _writer.end(); 660 } 661 662 /** 663 * Writes out all hidden values previously added by 664 * {@link #addHiddenValue(String, String, String)}, plus the allocated id list. 665 */ 666 667 protected void writeHiddenFieldList() 668 { 669 writeHiddenField(FORM_IDS, null, buildAllocatedIdList()); 670 671 Iterator i = _hiddenValues.iterator(); 672 while (i.hasNext()) 673 { 674 HiddenFieldData data = (HiddenFieldData) i.next(); 675 676 writeHiddenField(data.getName(), data.getId(), data.getValue()); 677 } 678 } 679 680 private void addHiddenFieldsForLinkParameter(ILink link, String parameterName) 681 { 682 String[] values = link.getParameterValues(parameterName); 683 684 // In some cases, there are no values, but a space is "reserved" for the provided name. 685 686 if (values == null) 687 return; 688 689 for (int i = 0; i < values.length; i++) 690 { 691 addHiddenValue(parameterName, values[i]); 692 } 693 } 694 695 protected void writeTag(IMarkupWriter writer, String method, String url) 696 { 697 writer.begin("form"); 698 writer.attribute("method", method); 699 writer.attribute("action", url); 700 } 701 702 public void prerenderField(IMarkupWriter writer, IComponent field, Location location) 703 { 704 Defense.notNull(writer, "writer"); 705 Defense.notNull(field, "field"); 706 707 String key = field.getExtendedId(); 708 709 if (_prerenderMap.containsKey(key)) 710 throw new ApplicationRuntimeException(FormMessages.fieldAlreadyPrerendered(field), 711 field, location, null); 712 713 NestedMarkupWriter nested = writer.getNestedWriter(); 714 715 field.render(nested, _cycle); 716 717 _prerenderMap.put(key, nested.getBuffer()); 718 } 719 720 public boolean wasPrerendered(IMarkupWriter writer, IComponent field) 721 { 722 String key = field.getExtendedId(); 723 724 // During a rewind, if the form is pre-rendered, the buffer will be null, 725 // so do the check based on the key, not a non-null value. 726 727 if (!_prerenderMap.containsKey(key)) 728 return false; 729 730 String buffer = (String) _prerenderMap.get(key); 731 732 writer.printRaw(buffer); 733 734 _prerenderMap.remove(key); 735 736 return true; 737 } 738 739 public void addDeferredRunnable(Runnable runnable) 740 { 741 Defense.notNull(runnable, "runnable"); 742 743 _deferredRunnables.add(runnable); 744 } 745 746 public void registerForFocus(IFormComponent field, int priority) 747 { 748 _delegate.registerForFocus(field, priority); 749 } 750 751}