Clover coverage report - Code Coverage for tapestry release 4.0-beta-2
Coverage timestamp: Sat Jul 9 2005 22:02:17 EDT
file stats: LOC: 661   Methods: 25
NCLOC: 359   Classes: 1
30 day Evaluation License registered to hlship@comcast.net Your 30 day evaluation period has expired. Please visit http://www.cenqua.com to obtain a licensed version of Clover
 
 Source file Conditionals Statements Methods TOTAL
FormSupportImpl.java 95.3% 94.1% 96% 94.5%
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   
 47    /**
 48    * Encapsulates most of the behavior of a Form component.
 49    *
 50    * @author Howard M. Lewis Ship
 51    * @since 4.0
 52    */
 53    public class FormSupportImpl implements FormSupport
 54    {
 55    /**
 56    * Name of query parameter storing the ids alloocated while rendering the form, as a comma
 57    * seperated list. This information is used when the form is submitted, to ensure that the
 58    * rewind allocates the exact same sequence of ids.
 59    */
 60   
 61    public static final String FORM_IDS = "formids";
 62   
 63    /**
 64    * Names of additional ids that were pre-reserved, as a comma-sepereated list. These are names
 65    * beyond that standard set. Certain engine services include extra parameter values that must be
 66    * accounted for, and page properties may be encoded as additional query parameters.
 67    */
 68   
 69    public static final String RESERVED_FORM_IDS = "reservedids";
 70   
 71    /**
 72    * Indicates why the form was submitted: whether for normal ("submit"), refresh, or because the
 73    * form was canceled.
 74    */
 75   
 76    public static final String SUBMIT_MODE = "submitmode";
 77   
 78    public static final String SCRIPT = "/org/apache/tapestry/form/Form.js";
 79   
 80    private final static Set _standardReservedIds;
 81   
 82    static
 83    {
 84  1 Set set = new HashSet();
 85   
 86  1 set.addAll(Arrays.asList(ServiceConstants.RESERVED_IDS));
 87  1 set.add(FORM_IDS);
 88  1 set.add(RESERVED_FORM_IDS);
 89  1 set.add(SUBMIT_MODE);
 90   
 91  1 _standardReservedIds = Collections.unmodifiableSet(set);
 92    }
 93   
 94    private final static Set _submitModes;
 95   
 96    static
 97    {
 98  1 Set set = new HashSet();
 99  1 set.add(FormConstants.SUBMIT_CANCEL);
 100  1 set.add(FormConstants.SUBMIT_NORMAL);
 101  1 set.add(FormConstants.SUBMIT_REFRESH);
 102   
 103  1 _submitModes = Collections.unmodifiableSet(set);
 104    }
 105   
 106    /**
 107    * Used when rewinding the form to figure to match allocated ids (allocated during the rewind)
 108    * against expected ids (allocated in the previous request cycle, when the form was rendered).
 109    */
 110   
 111    private int _allocatedIdIndex;
 112   
 113    /**
 114    * The list of allocated ids for form elements within this form. This list is constructed when a
 115    * form renders, and is validated against when the form is rewound.
 116    */
 117   
 118    private final List _allocatedIds = new ArrayList();
 119   
 120    private final IRequestCycle _cycle;
 121   
 122    private final IdAllocator _elementIdAllocator = new IdAllocator();
 123   
 124    private String _encodingType;
 125   
 126    private final List _deferredRunnables = new ArrayList();
 127   
 128    /**
 129    * Map keyed on extended component id, value is the pre-rendered markup for that component.
 130    */
 131   
 132    private final Map _prerenderMap = new HashMap();
 133   
 134    /**
 135    * {@link Map}, keyed on {@link FormEventType}. Values are either a String (the function name
 136    * of a single event handler), or a List of Strings (a sequence of event handler function
 137    * names).
 138    */
 139   
 140    private Map _events;
 141   
 142    private final IForm _form;
 143   
 144    private final List _hiddenValues = new ArrayList();
 145   
 146    private boolean _rewinding;
 147   
 148    private final IMarkupWriter _writer;
 149   
 150    private final Resource _script;
 151   
 152  90 public FormSupportImpl(IMarkupWriter writer, IRequestCycle cycle, IForm form)
 153    {
 154  90 Defense.notNull(writer, "writer");
 155  90 Defense.notNull(cycle, "cycle");
 156  90 Defense.notNull(form, "form");
 157   
 158  90 _writer = writer;
 159  90 _cycle = cycle;
 160  90 _form = form;
 161   
 162  90 _rewinding = cycle.isRewound(form);
 163  90 _allocatedIdIndex = 0;
 164   
 165  90 _script = new ClasspathResource(cycle.getEngine().getClassResolver(), SCRIPT);
 166    }
 167   
 168    /**
 169    * Adds an event handler for the form, of the given type.
 170    */
 171   
 172  6 public void addEventHandler(FormEventType type, String functionName)
 173    {
 174  6 if (_events == null)
 175  3 _events = new HashMap();
 176   
 177  6 List functionList = (List) _events.get(type);
 178   
 179    // The value can either be a String, or a List of String. Since
 180    // it is rare for there to be more than one event handling function,
 181    // we start with just a String.
 182   
 183  6 if (functionList == null)
 184    {
 185  3 functionList = new ArrayList();
 186   
 187  3 _events.put(type, functionList);
 188    }
 189   
 190  6 functionList.add(functionName);
 191    }
 192   
 193    /**
 194    * Adds hidden fields for parameters provided by the {@link ILink}. These parameters define the
 195    * information needed to dispatch the request, plus state information. The names of these
 196    * parameters must be reserved so that conflicts don't occur that could disrupt the request
 197    * processing. For example, if the id 'page' is not reserved, then a conflict could occur with a
 198    * component whose id is 'page'. A certain number of ids are always reserved, and we find any
 199    * additional ids beyond that set.
 200    */
 201   
 202  56 private void addHiddenFieldsForLinkParameters(ILink link)
 203    {
 204  56 String[] names = link.getParameterNames();
 205  56 int count = Tapestry.size(names);
 206   
 207  56 StringBuffer extraIds = new StringBuffer();
 208  56 String sep = "";
 209  56 boolean hasExtra = false;
 210   
 211    // All the reserved ids, which are essential for
 212    // dispatching the request, are automatically reserved.
 213    // Thus, if you have a component with an id of 'service', its element id
 214    // will likely be 'service$0'.
 215   
 216  56 preallocateReservedIds();
 217   
 218  56 for (int i = 0; i < count; i++)
 219    {
 220  286 String name = names[i];
 221   
 222    // Reserve the name.
 223   
 224  286 if (!_standardReservedIds.contains(name))
 225    {
 226  12 _elementIdAllocator.allocateId(name);
 227   
 228  12 extraIds.append(sep);
 229  12 extraIds.append(name);
 230   
 231  12 sep = ",";
 232  12 hasExtra = true;
 233    }
 234   
 235  286 addHiddenFieldsForLinkParameter(link, name);
 236    }
 237   
 238  56 if (hasExtra)
 239  12 addHiddenValue(RESERVED_FORM_IDS, extraIds.toString());
 240    }
 241   
 242  266 public void addHiddenValue(String name, String value)
 243    {
 244  266 _hiddenValues.add(new HiddenFieldData(name, value));
 245    }
 246   
 247  5 public void addHiddenValue(String name, String id, String value)
 248    {
 249  5 _hiddenValues.add(new HiddenFieldData(name, id, value));
 250    }
 251   
 252    /**
 253    * Converts the allocateIds property into a string, a comma-separated list of ids. This is
 254    * included as a hidden field in the form and is used to identify discrepencies when the form is
 255    * submitted.
 256    */
 257   
 258  51 private String buildAllocatedIdList()
 259    {
 260  51 StringBuffer buffer = new StringBuffer();
 261  51 int count = _allocatedIds.size();
 262   
 263  51 for (int i = 0; i < count; i++)
 264    {
 265  51 if (i > 0)
 266  15 buffer.append(',');
 267   
 268  51 buffer.append(_allocatedIds.get(i));
 269    }
 270   
 271  51 return buffer.toString();
 272    }
 273   
 274  51 private void emitEventHandlers(String eventManager)
 275    {
 276  51 if (_events == null || _events.isEmpty())
 277  48 return;
 278   
 279  3 PageRenderSupport pageRenderSupport = TapestryUtils.getPageRenderSupport(_cycle, _form);
 280   
 281  3 StringBuffer buffer = new StringBuffer();
 282   
 283  3 Iterator i = _events.entrySet().iterator();
 284   
 285  3 while (i.hasNext())
 286    {
 287  3 Map.Entry entry = (Map.Entry) i.next();
 288  3 FormEventType type = (FormEventType) entry.getKey();
 289  3 Object value = entry.getValue();
 290   
 291  3 buffer.append(eventManager);
 292  3 buffer.append(".");
 293  3 buffer.append(type.getAddListenerMethodName());
 294   
 295    // Build a composite function in-place
 296   
 297  3 buffer.append("(function (event)\n{");
 298   
 299  3 List l = (List) value;
 300  3 int count = l.size();
 301   
 302  3 for (int j = 0; j < count; j++)
 303    {
 304  6 String functionName = (String) l.get(j);
 305   
 306  6 if (j > 0)
 307    {
 308  3 buffer.append(";");
 309    }
 310   
 311  6 buffer.append("\n ");
 312  6 buffer.append(functionName);
 313   
 314    // It's supposed to be function names, but some of Paul's validation code
 315    // adds inline code to be executed instead.
 316   
 317  6 if (!functionName.endsWith(")"))
 318    {
 319  5 buffer.append("()");
 320    }
 321    }
 322   
 323  3 buffer.append(";\n});\n");
 324    }
 325   
 326  3 pageRenderSupport.addInitializationScript(buffer.toString());
 327    }
 328   
 329    /**
 330    * Constructs a unique identifier (within the Form). The identifier consists of the component's
 331    * id, with an index number added to ensure uniqueness.
 332    * <p>
 333    * Simply invokes
 334    * {@link #getElementId(org.apache.tapestry.form.IFormComponent, java.lang.String)}with the
 335    * component's id.
 336    */
 337   
 338  19 public String getElementId(IFormComponent component)
 339    {
 340  19 return getElementId(component, component.getId());
 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  99 public String getElementId(IFormComponent component, String baseId)
 353    {
 354  99 String result = _elementIdAllocator.allocateId(baseId);
 355   
 356  99 if (_rewinding)
 357    {
 358  46 if (_allocatedIdIndex >= _allocatedIds.size())
 359    {
 360  1 throw new StaleLinkException(FormMessages.formTooManyIds(_form, _allocatedIds
 361    .size(), component), component);
 362    }
 363   
 364  45 String expected = (String) _allocatedIds.get(_allocatedIdIndex);
 365   
 366  45 if (!result.equals(expected))
 367  2 throw new StaleLinkException(FormMessages.formIdMismatch(
 368    _form,
 369    _allocatedIdIndex,
 370    expected,
 371    result,
 372    component), component);
 373    }
 374    else
 375    {
 376  53 _allocatedIds.add(result);
 377    }
 378   
 379  96 _allocatedIdIndex++;
 380   
 381  96 component.setName(result);
 382   
 383  96 return result;
 384    }
 385   
 386  145 public boolean isRewinding()
 387    {
 388  145 return _rewinding;
 389    }
 390   
 391  89 private void preallocateReservedIds()
 392    {
 393  89 for (int i = 0; i < ServiceConstants.RESERVED_IDS.length; i++)
 394  534 _elementIdAllocator.allocateId(ServiceConstants.RESERVED_IDS[i]);
 395    }
 396   
 397    /**
 398    * Invoked when rewinding a form to re-initialize the _allocatedIds and _elementIdAllocator.
 399    * Converts a string passed as a parameter (and containing a comma separated list of ids) back
 400    * into the allocateIds property. In addition, return the state of the ID allocater back to
 401    * where it was at the start of the render.
 402    *
 403    * @see #buildAllocatedIdList()
 404    * @since 3.0
 405    */
 406   
 407  33 private void reinitializeIdAllocatorForRewind()
 408    {
 409  33 String allocatedFormIds = _cycle.getParameter(FORM_IDS);
 410   
 411  33 String[] ids = TapestryUtils.split(allocatedFormIds);
 412   
 413  33 for (int i = 0; i < ids.length; i++)
 414  50 _allocatedIds.add(ids[i]);
 415   
 416    // Now, reconstruct the the initial state of the
 417    // id allocator.
 418   
 419  33 preallocateReservedIds();
 420   
 421  33 String extraReservedIds = _cycle.getParameter(RESERVED_FORM_IDS);
 422   
 423  33 ids = TapestryUtils.split(extraReservedIds);
 424   
 425  33 for (int i = 0; i < ids.length; i++)
 426  3 _elementIdAllocator.allocateId(ids[i]);
 427    }
 428   
 429  56 public void render(String method, IRender informalParametersRenderer, ILink link)
 430    {
 431  56 String eventManager = emitEventManagerInitialization();
 432   
 433    // Convert the link's query parameters into a series of
 434    // hidden field values (that will be rendered later).
 435   
 436  56 addHiddenFieldsForLinkParameters(link);
 437   
 438    // Create a hidden field to store the submission mode, in case
 439    // client-side JavaScript forces an update.
 440   
 441  56 addHiddenValue(SUBMIT_MODE, null);
 442   
 443  56 IMarkupWriter nested = _writer.getNestedWriter();
 444   
 445  56 _form.renderBody(nested, _cycle);
 446   
 447  51 runDeferredRunnables();
 448   
 449  51 writeTag(_writer, method, link.getURL(null, false));
 450   
 451  51 _writer.attribute("name", _form.getName());
 452   
 453  51 if (_encodingType != null)
 454  3 _writer.attribute("enctype", _encodingType);
 455   
 456    // Write out event handlers collected during the rendering.
 457   
 458  51 emitEventHandlers(eventManager);
 459   
 460  51 informalParametersRenderer.render(_writer, _cycle);
 461   
 462    // Finish the <form> tag
 463   
 464  51 _writer.println();
 465   
 466  51 writeHiddenField(FORM_IDS, null, buildAllocatedIdList());
 467  51 writeHiddenFields();
 468   
 469    // Close the nested writer, inserting its contents.
 470   
 471  51 nested.close();
 472   
 473    // Close the <form> tag.
 474   
 475  51 _writer.end();
 476    }
 477   
 478    /**
 479    * Pre-renders the form, setting up some client-side form support. Returns the name of the
 480    * client-side form event manager variable.
 481    */
 482  50 protected String emitEventManagerInitialization()
 483    {
 484  50 PageRenderSupport pageRenderSupport = TapestryUtils.getOptionalPageRenderSupport(_cycle);
 485   
 486  50 if (pageRenderSupport == null)
 487  1 return null;
 488   
 489  49 pageRenderSupport.addExternalScript(_script);
 490   
 491  49 String formName = _form.getName();
 492   
 493  49 String eventManager = formName + "_events";
 494   
 495  49 pageRenderSupport.addInitializationScript("var " + eventManager
 496    + " = new FormEventManager(document." + formName + ");");
 497   
 498  49 return eventManager;
 499    }
 500   
 501  34 public String rewind()
 502    {
 503  34 _form.getDelegate().clear();
 504   
 505  34 String mode = _cycle.getParameter(SUBMIT_MODE);
 506   
 507    // On a cancel, don't bother rendering the body or anything else at all.
 508   
 509  34 if (FormConstants.SUBMIT_CANCEL.equals(mode))
 510  1 return mode;
 511   
 512  33 reinitializeIdAllocatorForRewind();
 513   
 514  33 _form.renderBody(_writer, _cycle);
 515   
 516  29 int expected = _allocatedIds.size();
 517   
 518    // The other case, _allocatedIdIndex > expected, is
 519    // checked for inside getElementId(). Remember that
 520    // _allocatedIdIndex is incremented after allocating.
 521   
 522  29 if (_allocatedIdIndex < expected)
 523    {
 524  1 String nextExpectedId = (String) _allocatedIds.get(_allocatedIdIndex);
 525   
 526  1 throw new StaleLinkException(FormMessages.formTooFewIds(_form, expected
 527    - _allocatedIdIndex, nextExpectedId), _form);
 528    }
 529   
 530  28 runDeferredRunnables();
 531   
 532  28 if (_submitModes.contains(mode))
 533  5 return mode;
 534   
 535    // Either something wacky on the client side, or a client without
 536    // javascript enabled.
 537   
 538  23 return FormConstants.SUBMIT_NORMAL;
 539   
 540    }
 541   
 542  79 private void runDeferredRunnables()
 543    {
 544  79 Iterator i = _deferredRunnables.iterator();
 545  79 while (i.hasNext())
 546    {
 547  2 Runnable r = (Runnable) i.next();
 548   
 549  2 r.run();
 550    }
 551    }
 552   
 553  5 public void setEncodingType(String encodingType)
 554    {
 555   
 556  5 if (_encodingType != null && !_encodingType.equals(encodingType))
 557  1 throw new ApplicationRuntimeException(FormMessages.encodingTypeContention(
 558    _form,
 559    _encodingType,
 560    encodingType), _form, null, null);
 561   
 562  4 _encodingType = encodingType;
 563    }
 564   
 565  264 protected void writeHiddenField(IMarkupWriter writer, String name, String id, String value)
 566    {
 567  264 writer.beginEmpty("input");
 568  264 writer.attribute("type", "hidden");
 569  264 writer.attribute("name", name);
 570   
 571  264 if (HiveMind.isNonBlank(id))
 572  2 writer.attribute("id", id);
 573   
 574  264 writer.attribute("value", value == null ? "" : value);
 575  264 writer.println();
 576    }
 577   
 578  300 private void writeHiddenField(String name, String id, String value)
 579    {
 580  300 writeHiddenField(_writer, name, id, value);
 581    }
 582   
 583    /**
 584    * Writes out all hidden values previously added by
 585    * {@link #addHiddenValue(String, String, String)}.
 586    */
 587   
 588  51 private void writeHiddenFields()
 589    {
 590  51 Iterator i = _hiddenValues.iterator();
 591  51 while (i.hasNext())
 592    {
 593  249 HiddenFieldData data = (HiddenFieldData) i.next();
 594   
 595  249 writeHiddenField(data.getName(), data.getId(), data.getValue());
 596    }
 597    }
 598   
 599  286 private void addHiddenFieldsForLinkParameter(ILink link, String parameterName)
 600    {
 601  286 String[] values = link.getParameterValues(parameterName);
 602   
 603    // In some cases, there are no values, but a space is "reserved" for the provided name.
 604   
 605  286 if (values == null)
 606  106 return;
 607   
 608  180 for (int i = 0; i < values.length; i++)
 609    {
 610  180 addHiddenValue(parameterName, values[i]);
 611    }
 612    }
 613   
 614  45 protected void writeTag(IMarkupWriter writer, String method, String url)
 615    {
 616  45 writer.begin("form");
 617  45 writer.attribute("method", method);
 618  45 writer.attribute("action", url);
 619    }
 620   
 621  0 public void prerenderField(IMarkupWriter writer, IComponent field, Location location)
 622    {
 623  0 Defense.notNull(writer, "writer");
 624  0 Defense.notNull(field, "field");
 625   
 626  0 String key = field.getExtendedId();
 627   
 628  0 if (_prerenderMap.containsKey(key))
 629  0 throw new ApplicationRuntimeException(FormMessages.fieldAlreadyPrerendered(field),
 630    location, null);
 631   
 632  0 NestedMarkupWriter nested = writer.getNestedWriter();
 633   
 634  0 field.render(nested, _cycle);
 635   
 636  0 _prerenderMap.put(key, nested.getBuffer());
 637    }
 638   
 639  75 public boolean wasPrerendered(IMarkupWriter writer, IComponent field)
 640    {
 641  75 String key = field.getExtendedId();
 642   
 643  75 String buffer = (String) _prerenderMap.get(key);
 644   
 645  75 if (buffer == null)
 646  75 return false;
 647   
 648  0 writer.printRaw(buffer);
 649   
 650  0 _prerenderMap.remove(key);
 651   
 652  0 return true;
 653    }
 654   
 655  2 public void addDeferredRunnable(Runnable runnable)
 656    {
 657  2 Defense.notNull(runnable, "runnable");
 658   
 659  2 _deferredRunnables.add(runnable);
 660    }
 661    }