Clover coverage report - Code Coverage for tapestry release 4.0-beta-9
Coverage timestamp: Sat Oct 1 2005 08:36:20 EDT
file stats: LOC: 719   Methods: 27
NCLOC: 380   Classes: 1
 
 Source file Conditionals Statements Methods TOTAL
FormSupportImpl.java 95.6% 93.4% 92.6% 93.8%
coverage coverage
 1    // Copyright 2005 The Apache Software Foundation
 2    //
 3    // Licensed under the Apache License, Version 2.0 (the "License");
 4    // you may not use this file except in compliance with the License.
 5    // You may obtain a copy of the License at
 6    //
 7    // http://www.apache.org/licenses/LICENSE-2.0
 8    //
 9    // Unless required by applicable law or agreed to in writing, software
 10    // distributed under the License is distributed on an "AS IS" BASIS,
 11    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12    // See the License for the specific language governing permissions and
 13    // limitations under the License.
 14   
 15    package org.apache.tapestry.form;
 16   
 17    import java.util.ArrayList;
 18    import java.util.Arrays;
 19    import java.util.Collections;
 20    import java.util.HashMap;
 21    import java.util.HashSet;
 22    import java.util.Iterator;
 23    import java.util.List;
 24    import java.util.Map;
 25    import java.util.Set;
 26   
 27    import org.apache.hivemind.ApplicationRuntimeException;
 28    import org.apache.hivemind.HiveMind;
 29    import org.apache.hivemind.Location;
 30    import org.apache.hivemind.Resource;
 31    import org.apache.hivemind.util.ClasspathResource;
 32    import org.apache.hivemind.util.Defense;
 33    import org.apache.tapestry.IComponent;
 34    import org.apache.tapestry.IForm;
 35    import org.apache.tapestry.IMarkupWriter;
 36    import org.apache.tapestry.IRender;
 37    import org.apache.tapestry.IRequestCycle;
 38    import org.apache.tapestry.NestedMarkupWriter;
 39    import org.apache.tapestry.PageRenderSupport;
 40    import org.apache.tapestry.StaleLinkException;
 41    import org.apache.tapestry.Tapestry;
 42    import org.apache.tapestry.TapestryUtils;
 43    import org.apache.tapestry.engine.ILink;
 44    import org.apache.tapestry.services.ServiceConstants;
 45    import org.apache.tapestry.util.IdAllocator;
 46    import org.apache.tapestry.valid.IValidationDelegate;
 47   
 48    /**
 49    * Encapsulates most of the behavior of a Form component.
 50    *
 51    * @author Howard M. Lewis Ship
 52    * @since 4.0
 53    */
 54    public class FormSupportImpl implements FormSupport
 55    {
 56    /**
 57    * Name of query parameter storing the ids alloocated while rendering the form, as a comma
 58    * seperated list. This information is used when the form is submitted, to ensure that the
 59    * rewind allocates the exact same sequence of ids.
 60    */
 61   
 62    public static final String FORM_IDS = "formids";
 63   
 64    /**
 65    * Names of additional ids that were pre-reserved, as a comma-sepereated list. These are names
 66    * beyond that standard set. Certain engine services include extra parameter values that must be
 67    * accounted for, and page properties may be encoded as additional query parameters.
 68    */
 69   
 70    public static final String RESERVED_FORM_IDS = "reservedids";
 71   
 72    /**
 73    * Indicates why the form was submitted: whether for normal ("submit"), refresh, or because the
 74    * form was canceled.
 75    */
 76   
 77    public static final String SUBMIT_MODE = "submitmode";
 78   
 79    public static final String SCRIPT = "/org/apache/tapestry/form/Form.js";
 80   
 81    private final static Set _standardReservedIds;
 82   
 83    /**
 84    * Attribute set to true when a field has been focused; used to prevent conflicting JavaScript
 85    * for field focusing from being emitted.
 86    */
 87   
 88    public static final String FIELD_FOCUS_ATTRIBUTE = "org.apache.tapestry.field-focused";
 89   
 90    static
 91    {
 92  1 Set set = new HashSet();
 93   
 94  1 set.addAll(Arrays.asList(ServiceConstants.RESERVED_IDS));
 95  1 set.add(FORM_IDS);
 96  1 set.add(RESERVED_FORM_IDS);
 97  1 set.add(SUBMIT_MODE);
 98   
 99  1 _standardReservedIds = Collections.unmodifiableSet(set);
 100    }
 101   
 102    private final static Set _submitModes;
 103   
 104    static
 105    {
 106  1 Set set = new HashSet();
 107  1 set.add(FormConstants.SUBMIT_CANCEL);
 108  1 set.add(FormConstants.SUBMIT_NORMAL);
 109  1 set.add(FormConstants.SUBMIT_REFRESH);
 110   
 111  1 _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  92 public FormSupportImpl(IMarkupWriter writer, IRequestCycle cycle, IForm form)
 165    {
 166  92 Defense.notNull(writer, "writer");
 167  92 Defense.notNull(cycle, "cycle");
 168  92 Defense.notNull(form, "form");
 169   
 170  92 _writer = writer;
 171  92 _cycle = cycle;
 172  92 _form = form;
 173  92 _delegate = form.getDelegate();
 174   
 175  92 _rewinding = cycle.isRewound(form);
 176  92 _allocatedIdIndex = 0;
 177   
 178  92 _script = new ClasspathResource(cycle.getEngine().getClassResolver(), SCRIPT);
 179   
 180  92 _pageRenderSupport = TapestryUtils.getOptionalPageRenderSupport(cycle);
 181    }
 182   
 183    /**
 184    * Adds an event handler for the form, of the given type.
 185    */
 186   
 187  6 public void addEventHandler(FormEventType type, String functionName)
 188    {
 189  6 if (_events == null)
 190  3 _events = new HashMap();
 191   
 192  6 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  6 if (functionList == null)
 199    {
 200  3 functionList = new ArrayList();
 201   
 202  3 _events.put(type, functionList);
 203    }
 204   
 205  6 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  57 private void addHiddenFieldsForLinkParameters(ILink link)
 218    {
 219  57 String[] names = link.getParameterNames();
 220  57 int count = Tapestry.size(names);
 221   
 222  57 StringBuffer extraIds = new StringBuffer();
 223  57 String sep = "";
 224  57 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  57 preallocateReservedIds();
 232   
 233  57 for (int i = 0; i < count; i++)
 234    {
 235  292 String name = names[i];
 236   
 237    // Reserve the name.
 238   
 239  292 if (!_standardReservedIds.contains(name))
 240    {
 241  12 _elementIdAllocator.allocateId(name);
 242   
 243  12 extraIds.append(sep);
 244  12 extraIds.append(name);
 245   
 246  12 sep = ",";
 247  12 hasExtra = true;
 248    }
 249   
 250  292 addHiddenFieldsForLinkParameter(link, name);
 251    }
 252   
 253  57 if (hasExtra)
 254  12 addHiddenValue(RESERVED_FORM_IDS, extraIds.toString());
 255    }
 256   
 257  283 public void addHiddenValue(String name, String value)
 258    {
 259  283 _hiddenValues.add(new HiddenFieldData(name, value));
 260    }
 261   
 262  5 public void addHiddenValue(String name, String id, String value)
 263    {
 264  5 _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  52 private String buildAllocatedIdList()
 274    {
 275  52 StringBuffer buffer = new StringBuffer();
 276  52 int count = _allocatedIds.size();
 277   
 278  52 for (int i = 0; i < count; i++)
 279    {
 280  60 if (i > 0)
 281  23 buffer.append(',');
 282   
 283  60 buffer.append(_allocatedIds.get(i));
 284    }
 285   
 286  52 return buffer.toString();
 287    }
 288   
 289  52 private void emitEventHandlers(String formId)
 290    {
 291  52 if (_events == null || _events.isEmpty())
 292  49 return;
 293   
 294  3 StringBuffer buffer = new StringBuffer();
 295   
 296  3 Iterator i = _events.entrySet().iterator();
 297   
 298  3 while (i.hasNext())
 299    {
 300  3 Map.Entry entry = (Map.Entry) i.next();
 301  3 FormEventType type = (FormEventType) entry.getKey();
 302  3 Object value = entry.getValue();
 303   
 304  3 buffer.append("Tapestry.");
 305  3 buffer.append(type.getAddHandlerFunctionName());
 306  3 buffer.append("('");
 307  3 buffer.append(formId);
 308  3 buffer.append("', function (event)\n{");
 309   
 310  3 List l = (List) value;
 311  3 int count = l.size();
 312   
 313  3 for (int j = 0; j < count; j++)
 314    {
 315  6 String functionName = (String) l.get(j);
 316   
 317  6 if (j > 0)
 318    {
 319  3 buffer.append(";");
 320    }
 321   
 322  6 buffer.append("\n ");
 323  6 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  6 if (!functionName.endsWith(")"))
 329    {
 330  5 buffer.append("()");
 331    }
 332    }
 333   
 334  3 buffer.append(";\n});\n");
 335    }
 336   
 337    // TODO: If PRS is null ...
 338   
 339  3 _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  19 public String getElementId(IFormComponent component)
 352    {
 353  19 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  115 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  115 String filteredId = TapestryUtils.convertTapestryIdToNMToken(baseId);
 370   
 371  115 String result = _elementIdAllocator.allocateId(filteredId);
 372   
 373  115 if (_rewinding)
 374    {
 375  53 if (_allocatedIdIndex >= _allocatedIds.size())
 376    {
 377  1 throw new StaleLinkException(FormMessages.formTooManyIds(_form, _allocatedIds
 378    .size(), component), component);
 379    }
 380   
 381  52 String expected = (String) _allocatedIds.get(_allocatedIdIndex);
 382   
 383  52 if (!result.equals(expected))
 384  2 throw new StaleLinkException(FormMessages.formIdMismatch(
 385    _form,
 386    _allocatedIdIndex,
 387    expected,
 388    result,
 389    component), component);
 390    }
 391    else
 392    {
 393  62 _allocatedIds.add(result);
 394    }
 395   
 396  112 _allocatedIdIndex++;
 397   
 398  112 component.setName(result);
 399   
 400  112 return result;
 401    }
 402   
 403  154 public boolean isRewinding()
 404    {
 405  154 return _rewinding;
 406    }
 407   
 408  91 private void preallocateReservedIds()
 409    {
 410  91 for (int i = 0; i < ServiceConstants.RESERVED_IDS.length; i++)
 411  546 _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  34 private void reinitializeIdAllocatorForRewind()
 425    {
 426  34 String allocatedFormIds = _cycle.getParameter(FORM_IDS);
 427   
 428  34 String[] ids = TapestryUtils.split(allocatedFormIds);
 429   
 430  34 for (int i = 0; i < ids.length; i++)
 431  57 _allocatedIds.add(ids[i]);
 432   
 433    // Now, reconstruct the the initial state of the
 434    // id allocator.
 435   
 436  34 preallocateReservedIds();
 437   
 438  34 String extraReservedIds = _cycle.getParameter(RESERVED_FORM_IDS);
 439   
 440  34 ids = TapestryUtils.split(extraReservedIds);
 441   
 442  34 for (int i = 0; i < ids.length; i++)
 443  3 _elementIdAllocator.allocateId(ids[i]);
 444    }
 445   
 446  57 public void render(String method, IRender informalParametersRenderer, ILink link)
 447    {
 448  57 String formId = _form.getName();
 449   
 450  57 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  57 addHiddenFieldsForLinkParameters(link);
 456   
 457    // Create a hidden field to store the submission mode, in case
 458    // client-side JavaScript forces an update.
 459   
 460  57 addHiddenValue(SUBMIT_MODE, null);
 461   
 462  57 IMarkupWriter nested = _writer.getNestedWriter();
 463   
 464  57 _form.renderBody(nested, _cycle);
 465   
 466  52 runDeferredRunnables();
 467   
 468  52 writeTag(_writer, method, link.getURL(null, false));
 469   
 470    // For HTML compatibility
 471  52 _writer.attribute("name", formId);
 472   
 473    // For XHTML compatibility
 474  52 _writer.attribute("id", formId);
 475   
 476  52 if (_encodingType != null)
 477  3 _writer.attribute("enctype", _encodingType);
 478   
 479    // Write out event handlers collected during the rendering.
 480   
 481  52 emitEventHandlers(formId);
 482   
 483  52 informalParametersRenderer.render(_writer, _cycle);
 484   
 485    // Finish the <form> tag
 486   
 487  52 _writer.println();
 488   
 489  52 writeHiddenFields();
 490   
 491    // Close the nested writer, inserting its contents.
 492   
 493  52 nested.close();
 494   
 495    // Close the <form> tag.
 496   
 497  52 _writer.end();
 498   
 499  52 String fieldId = _delegate.getFocusField();
 500   
 501  52 if (fieldId == null || _pageRenderSupport == null)
 502  42 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  10 if (!_form.getFocus() || _cycle.getAttribute(FIELD_FOCUS_ATTRIBUTE) != null)
 508  1 return;
 509   
 510  9 _pageRenderSupport.addInitializationScript("Tapestry.set_focus('" + fieldId + "');");
 511   
 512  9 _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  57 protected void emitEventManagerInitialization(String formId)
 520    {
 521  57 if (_pageRenderSupport == null)
 522  8 return;
 523   
 524  49 _pageRenderSupport.addExternalScript(_script);
 525   
 526  49 _pageRenderSupport.addInitializationScript("Tapestry.register_form('" + formId + "');");
 527    }
 528   
 529  35 public String rewind()
 530    {
 531  35 _form.getDelegate().clear();
 532   
 533  35 String mode = _cycle.getParameter(SUBMIT_MODE);
 534   
 535    // On a cancel, don't bother rendering the body or anything else at all.
 536   
 537  35 if (FormConstants.SUBMIT_CANCEL.equals(mode))
 538  1 return mode;
 539   
 540  34 reinitializeIdAllocatorForRewind();
 541   
 542  34 _form.renderBody(_writer, _cycle);
 543   
 544  30 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  30 if (_allocatedIdIndex < expected)
 551    {
 552  1 String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex);
 553   
 554  1 throw new StaleLinkException(FormMessages.formTooFewIds(_form, expected
 555    - _allocatedIdIndex, nextExpectedId), _form);
 556    }
 557   
 558  29 runDeferredRunnables();
 559   
 560  29 if (_submitModes.contains(mode))
 561  5 return mode;
 562   
 563    // Either something wacky on the client side, or a client without
 564    // javascript enabled.
 565   
 566  24 return FormConstants.SUBMIT_NORMAL;
 567   
 568    }
 569   
 570  81 private void runDeferredRunnables()
 571    {
 572  81 Iterator i = _deferredRunnables.iterator();
 573  81 while (i.hasNext())
 574    {
 575  2 Runnable r = (Runnable) i.next();
 576   
 577  2 r.run();
 578    }
 579    }
 580   
 581  5 public void setEncodingType(String encodingType)
 582    {
 583   
 584  5 if (_encodingType != null && !_encodingType.equals(encodingType))
 585  1 throw new ApplicationRuntimeException(FormMessages.encodingTypeContention(
 586    _form,
 587    _encodingType,
 588    encodingType), _form, null, null);
 589   
 590  4 _encodingType = encodingType;
 591    }
 592   
 593    /**
 594    * Overwridden by {@link org.apache.tapestry.wml.GoFormSupportImpl} (WML).
 595    */
 596  282 protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value)
 597    {
 598  282 writer.beginEmpty("input");
 599  282 writer.attribute("type", "hidden");
 600  282 writer.attribute("name", name);
 601   
 602  282 if (HiveMind.isNonBlank(id))
 603  2 writer.attribute("id", id);
 604   
 605  282 writer.attribute("value", value == null ? "" : value);
 606  282 writer.println();
 607    }
 608   
 609  318 private void writeHiddenField(String name, String id, String value)
 610    {
 611  318 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 &lt;div&gt; tag around
 617    * {@link #writeHiddenFieldList()}. Overriden by
 618    * {@link org.apache.tapestry.wml.GoFormSupportImpl}.
 619    */
 620   
 621  46 protected void writeHiddenFields()
 622    {
 623  46 _writer.begin("div");
 624   
 625  46 writeHiddenFieldList();
 626   
 627  46 _writer.end();
 628    }
 629   
 630    /**
 631    * Writes out all hidden values previously added by
 632    * {@link #addHiddenValue(String, String, String)}, plus the allocated id list.
 633    */
 634   
 635  52 protected void writeHiddenFieldList()
 636    {
 637  52 writeHiddenField(FORM_IDS, null, buildAllocatedIdList());
 638   
 639  52 Iterator i = _hiddenValues.iterator();
 640  52 while (i.hasNext())
 641    {
 642  266 HiddenFieldData data = (HiddenFieldData) i.next();
 643   
 644  266 writeHiddenField(data.getName(), data.getId(), data.getValue());
 645    }
 646    }
 647   
 648  292 private void addHiddenFieldsForLinkParameter(ILink link, String parameterName)
 649    {
 650  292 String[] values = link.getParameterValues(parameterName);
 651   
 652    // In some cases, there are no values, but a space is "reserved" for the provided name.
 653   
 654  292 if (values == null)
 655  109 return;
 656   
 657  183 for (int i = 0; i < values.length; i++)
 658    {
 659  183 addHiddenValue(parameterName, values[i]);
 660    }
 661    }
 662   
 663  46 protected void writeTag(IMarkupWriter writer, String method, String url)
 664    {
 665  46 writer.begin("form");
 666  46 writer.attribute("method", method);
 667  46 writer.attribute("action", url);
 668    }
 669   
 670  0 public void prerenderField(IMarkupWriter writer, IComponent field, Location location)
 671    {
 672  0 Defense.notNull(writer, "writer");
 673  0 Defense.notNull(field, "field");
 674   
 675  0 String key = field.getExtendedId();
 676   
 677  0 if (_prerenderMap.containsKey(key))
 678  0 throw new ApplicationRuntimeException(FormMessages.fieldAlreadyPrerendered(field),
 679    location, null);
 680   
 681  0 NestedMarkupWriter nested = writer.getNestedWriter();
 682   
 683  0 field.render(nested, _cycle);
 684   
 685  0 _prerenderMap.put(key, nested.getBuffer());
 686    }
 687   
 688  75 public boolean wasPrerendered(IMarkupWriter writer, IComponent field)
 689    {
 690  75 String key = field.getExtendedId();
 691   
 692    // During a rewind, if the form is pre-rendered, the buffer will be null,
 693    // so do the check based on the key, not a non-null value.
 694   
 695  75 if (!_prerenderMap.containsKey(key))
 696  75 return false;
 697   
 698  0 String buffer = (String) _prerenderMap.get(key);
 699   
 700  0 writer.printRaw(buffer);
 701   
 702  0 _prerenderMap.remove(key);
 703   
 704  0 return true;
 705    }
 706   
 707  2 public void addDeferredRunnable(Runnable runnable)
 708    {
 709  2 Defense.notNull(runnable, "runnable");
 710   
 711  2 _deferredRunnables.add(runnable);
 712    }
 713   
 714  0 public void registerForFocus(IFormComponent field, int priority)
 715    {
 716  0 _delegate.registerForFocus(field, priority);
 717    }
 718   
 719    }