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