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