Coverage Report - org.apache.tapestry5.corelib.components.AjaxFormLoop
 
Classes in this File Line Coverage Branch Coverage Complexity
AjaxFormLoop
93%
52/56
60%
6/10
0
AjaxFormLoop$1
100%
14/14
100%
2/2
0
AjaxFormLoop$10
100%
5/5
N/A
0
AjaxFormLoop$2
25%
1/4
N/A
0
AjaxFormLoop$3
33%
1/3
N/A
0
AjaxFormLoop$4
25%
1/4
N/A
0
AjaxFormLoop$5
33%
1/3
N/A
0
AjaxFormLoop$6
100%
4/4
N/A
0
AjaxFormLoop$7
100%
3/3
N/A
0
AjaxFormLoop$8
33%
1/3
N/A
0
AjaxFormLoop$9
100%
3/3
N/A
0
AjaxFormLoop$SyncValue
86%
6/7
N/A
0
 
 1  
 // Copyright 2008, 2009 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.tapestry5.corelib.components;
 16  
 
 17  
 import org.apache.tapestry5.*;
 18  
 import org.apache.tapestry5.annotations.*;
 19  
 import org.apache.tapestry5.corelib.internal.AjaxFormLoopContext;
 20  
 import org.apache.tapestry5.internal.services.PageRenderQueue;
 21  
 import org.apache.tapestry5.ioc.annotations.Inject;
 22  
 import org.apache.tapestry5.ioc.internal.util.Defense;
 23  
 import org.apache.tapestry5.ioc.services.TypeCoercer;
 24  
 import org.apache.tapestry5.json.JSONArray;
 25  
 import org.apache.tapestry5.json.JSONObject;
 26  
 import org.apache.tapestry5.services.*;
 27  
 
 28  
 import java.util.Collections;
 29  
 import java.util.Iterator;
 30  
 
 31  
 /**
 32  
  * A special form of the {@link org.apache.tapestry5.corelib.components.Loop} component that adds  Ajax support to
 33  
  * handle adding new rows and removing existing rows dynamically.  Expects that the values being iterated over are
 34  
  * entities that can be identified via a {@link org.apache.tapestry5.ValueEncoder}.
 35  
  * <p/>
 36  
  * Works with {@link org.apache.tapestry5.corelib.components.AddRowLink} and {@link
 37  
  * org.apache.tapestry5.corelib.components.RemoveRowLink} components.
 38  
  * <p/>
 39  
  * The addRow event will receive the context specified by the context parameter.
 40  
  * <p/>
 41  
  * The removeRow event will receive the client-side value for the row being iterated.
 42  
  *
 43  
  * @see EventConstants#ADD_ROW
 44  
  * @see EventConstants#REMOVE_ROW
 45  
  */
 46  
 @Events({ EventConstants.ADD_ROW, EventConstants.REMOVE_ROW })
 47  48
 public class AjaxFormLoop
 48  
 {
 49  
     /**
 50  
      * The element to render for each iteration of the loop. The default comes from the template, or "div" if the
 51  
      * template did not specify an element.
 52  
      */
 53  
     @Parameter(defaultPrefix = BindingConstants.LITERAL)
 54  
     @Property(write = false)
 55  
     private String element;
 56  
 
 57  
     /**
 58  
      * The objects to iterate over (passed to the internal Loop component).
 59  
      */
 60  
     @Parameter(required = true, autoconnect = true)
 61  
     private Iterable source;
 62  
 
 63  
     /**
 64  
      * The current value from the source.
 65  
      */
 66  
     @Parameter(required = true)
 67  
     private Object value;
 68  
 
 69  
     /**
 70  
      * Name of a function on the client-side Tapestry.ElementEffect object that is invoked to make added content
 71  
      * visible.  This is used with the {@link FormInjector} component, when adding a new row to the loop. Leaving as
 72  
      * null uses the default function, "highlight".
 73  
      */
 74  
     @Parameter(defaultPrefix = BindingConstants.LITERAL)
 75  
     private String show;
 76  
 
 77  
     /**
 78  
      * The context for the form loop (optional parameter). This list of values will be converted into strings and
 79  
      * included in the URI. The strings will be coerced back to whatever their values are and made available to event
 80  
      * handler methods.
 81  
      */
 82  
     @Parameter
 83  
     private Object[] context;
 84  
 
 85  
 
 86  
     /**
 87  
      * A block to render after the loop as the body of the {@link org.apache.tapestry5.corelib.components.FormInjector}.
 88  
      * This typically contains a {@link org.apache.tapestry5.corelib.components.AddRowLink}.
 89  
      */
 90  
     @Parameter(value = "block:defaultAddRow", defaultPrefix = BindingConstants.LITERAL)
 91  
     @Property(write = false)
 92  
     private Block addRow;
 93  
 
 94  
     /**
 95  
      * The block that contains the form injector (it is rendered last, as the "tail" of the AjaxFormLoop). This, in
 96  
      * turn, references the addRow block (from a parameter, or a default).
 97  
      */
 98  
     @Inject
 99  
     private Block tail;
 100  
 
 101  
     /**
 102  
      * Required parameter used to convert server-side objects (provided from the source) into client-side ids and back.
 103  
      * A default encoder may be calculated from the type of property bound to the value parameter.
 104  
      */
 105  
     @Parameter(required = true, allowNull = false)
 106  
     private ValueEncoder<Object> encoder;
 107  
 
 108  
     @InjectComponent
 109  
     private ClientElement rowInjector;
 110  
 
 111  
     @InjectComponent
 112  
     private FormFragment fragment;
 113  
 
 114  
     @Inject
 115  
     private Block ajaxResponse;
 116  
 
 117  
     @Inject
 118  
     private ComponentResources resources;
 119  
 
 120  
     @Environmental
 121  
     private FormSupport formSupport;
 122  
 
 123  
     @Environmental
 124  
     private Heartbeat heartbeat;
 125  
 
 126  
     @Inject
 127  
     private Environment environment;
 128  
 
 129  
     @Inject
 130  
     private RenderSupport renderSupport;
 131  
 
 132  
     private JSONArray addRowTriggers;
 133  
 
 134  
     private Iterator iterator;
 135  
 
 136  
     @Inject
 137  
     private TypeCoercer typeCoercer;
 138  
 
 139  
     @Inject
 140  
     private ComponentDefaultProvider defaultProvider;
 141  
 
 142  
     @Inject
 143  
     private PageRenderQueue pageRenderQueue;
 144  
 
 145  
     private boolean renderingInjector;
 146  
 
 147  
     ValueEncoder defaultEncoder()
 148  
     {
 149  0
         return defaultProvider.defaultValueEncoder("value", resources);
 150  
     }
 151  
 
 152  
 
 153  2
     private final AjaxFormLoopContext formLoopContext = new AjaxFormLoopContext()
 154  
     {
 155  
         public void addAddRowTrigger(String clientId)
 156  
         {
 157  6
             Defense.notBlank(clientId, "clientId");
 158  
 
 159  6
             addRowTriggers.put(clientId);
 160  6
         }
 161  
 
 162  
         private String currentFragmentId()
 163  
         {
 164  4
             ClientElement element = renderingInjector ? rowInjector : fragment;
 165  
 
 166  4
             return element.getClientId();
 167  
         }
 168  
 
 169  2
         public void addRemoveRowTrigger(String clientId)
 170  
         {
 171  4
             Link link = resources.createEventLink("triggerRemoveRow", toClientValue());
 172  
 
 173  4
             String asURI = link.toAbsoluteURI();
 174  
 
 175  4
             JSONObject spec = new JSONObject();
 176  4
             spec.put("link", clientId);
 177  4
             spec.put("fragment", currentFragmentId());
 178  4
             spec.put("url", asURI);
 179  
 
 180  4
             renderSupport.addInit("formLoopRemoveLink", spec);
 181  4
         }
 182  
     };
 183  
 
 184  
 
 185  
     String defaultElement()
 186  
     {
 187  2
         return resources.getElementName("div");
 188  
     }
 189  
 
 190  
 
 191  
     /**
 192  
      * Action for synchronizing the current element of the loop by recording its client value.
 193  
      */
 194  2
     static class SyncValue implements ComponentAction<AjaxFormLoop>
 195  
     {
 196  
         private final String clientValue;
 197  
 
 198  
         public SyncValue(String clientValue)
 199  4
         {
 200  4
             this.clientValue = clientValue;
 201  4
         }
 202  
 
 203  
         public void execute(AjaxFormLoop component)
 204  
         {
 205  2
             component.syncValue(clientValue);
 206  2
         }
 207  
 
 208  
         @Override
 209  
         public String toString()
 210  
         {
 211  0
             return String.format("AjaxFormLoop.SyncValue[%s]", clientValue);
 212  
         }
 213  
     }
 214  
 
 215  2
     private static final ComponentAction<AjaxFormLoop> BEGIN_HEARTBEAT = new ComponentAction<AjaxFormLoop>()
 216  
     {
 217  
         public void execute(AjaxFormLoop component)
 218  
         {
 219  0
             component.beginHeartbeat();
 220  0
         }
 221  
 
 222  
         @Override
 223  2
         public String toString()
 224  
         {
 225  0
             return "AjaxFormLoop.BeginHeartbeat";
 226  
         }
 227  
     };
 228  
 
 229  
     @Property(write = false)
 230  2
     private final Renderable beginHeartbeat = new Renderable()
 231  
     {
 232  2
         public void render(MarkupWriter writer)
 233  
         {
 234  0
             formSupport.storeAndExecute(AjaxFormLoop.this, BEGIN_HEARTBEAT);
 235  0
         }
 236  
     };
 237  
 
 238  2
     private static final ComponentAction<AjaxFormLoop> END_HEARTBEAT = new ComponentAction<AjaxFormLoop>()
 239  
     {
 240  
         public void execute(AjaxFormLoop component)
 241  
         {
 242  0
             component.endHeartbeat();
 243  0
         }
 244  
 
 245  
         @Override
 246  2
         public String toString()
 247  
         {
 248  0
             return "AjaxFormLoop.EndHeartbeat";
 249  
         }
 250  
     };
 251  
 
 252  
     @Property(write = false)
 253  2
     private final Renderable endHeartbeat = new Renderable()
 254  
     {
 255  2
         public void render(MarkupWriter writer)
 256  
         {
 257  0
             formSupport.storeAndExecute(AjaxFormLoop.this, END_HEARTBEAT);
 258  0
         }
 259  
     };
 260  
 
 261  
     @Property(write = false)
 262  2
     private final Renderable beforeBody = new Renderable()
 263  
     {
 264  2
         public void render(MarkupWriter writer)
 265  
         {
 266  4
             beginHeartbeat();
 267  4
             syncCurrentValue();
 268  4
         }
 269  
     };
 270  
 
 271  
     @Property(write = false)
 272  2
     private final Renderable afterBody = new Renderable()
 273  
     {
 274  2
         public void render(MarkupWriter writer)
 275  
         {
 276  4
             endHeartbeat();
 277  4
         }
 278  
     };
 279  
 
 280  
     @SuppressWarnings({ "unchecked" })
 281  
     @Log
 282  
     private void syncValue(String clientValue)
 283  
     {
 284  2
         Object value = encoder.toValue(clientValue);
 285  
 
 286  2
         if (value == null)
 287  0
             throw new RuntimeException(
 288  
                     String.format("Unable to convert client value '%s' back into a server-side object.", clientValue));
 289  
 
 290  2
         this.value = value;
 291  2
     }
 292  
 
 293  
     @Property(write = false)
 294  2
     private final Renderable syncValue = new Renderable()
 295  
     {
 296  2
         public void render(MarkupWriter writer)
 297  
         {
 298  0
             syncCurrentValue();
 299  0
         }
 300  
     };
 301  
 
 302  
     private void syncCurrentValue()
 303  
     {
 304  4
         String id = toClientValue();
 305  
 
 306  
         // Add the command that restores value from the value clientValue,
 307  
         // when the form is submitted.
 308  
 
 309  4
         formSupport.store(this, new SyncValue(id));
 310  4
     }
 311  
 
 312  
     /**
 313  
      * Uses the {@link org.apache.tapestry5.ValueEncoder} to convert the current server-side value to a client-side
 314  
      * value.
 315  
      */
 316  
     @SuppressWarnings({ "unchecked" })
 317  
     private String toClientValue()
 318  
     {
 319  8
         return encoder.toClient(value);
 320  
     }
 321  
 
 322  
 
 323  
     void setupRender()
 324  
     {
 325  6
         addRowTriggers = new JSONArray();
 326  
 
 327  6
         pushContext();
 328  
 
 329  6
         iterator = source == null
 330  
                    ? Collections.EMPTY_LIST.iterator()
 331  
                    : source.iterator();
 332  
 
 333  6
         renderingInjector = false;
 334  6
     }
 335  
 
 336  
     private void pushContext()
 337  
     {
 338  8
         environment.push(AjaxFormLoopContext.class, formLoopContext);
 339  8
     }
 340  
 
 341  
     boolean beginRender(MarkupWriter writer)
 342  
     {
 343  6
         if (!iterator.hasNext()) return false;
 344  
 
 345  2
         value = iterator.next();
 346  
 
 347  2
         return true;  // Render body, etc.
 348  
     }
 349  
 
 350  
     Object afterRender(MarkupWriter writer)
 351  
     {
 352  
         // When out of source items to render, switch over to the addRow block (either the default,
 353  
         // or from the addRow parameter) before proceeding to cleanup render.
 354  
 
 355  6
         if (!iterator.hasNext())
 356  
         {
 357  6
             renderingInjector = true;
 358  6
             return tail;
 359  
         }
 360  
 
 361  
         // There's more to come, loop back to begin render.
 362  
 
 363  0
         return false;
 364  
     }
 365  
 
 366  
     void cleanupRender()
 367  
     {
 368  6
         popContext();
 369  
 
 370  6
         JSONObject spec = new JSONObject();
 371  
 
 372  6
         spec.put("rowInjector", rowInjector.getClientId());
 373  6
         spec.put("addRowTriggers", addRowTriggers);
 374  
 
 375  6
         renderSupport.addInit("ajaxFormLoop", spec);
 376  6
     }
 377  
 
 378  
     private void popContext()
 379  
     {
 380  8
         environment.pop(AjaxFormLoopContext.class);
 381  8
     }
 382  
 
 383  
     /**
 384  
      * When the action event arrives from the FormInjector, we fire our own event, "addRow" to tell the container to add
 385  
      * a new row, and to return that new entity for rendering.
 386  
      */
 387  
     @Log
 388  
     Object onActionFromRowInjector(EventContext context)
 389  
     {
 390  2
         ComponentEventCallback callback = new ComponentEventCallback()
 391  
         {
 392  2
             public boolean handleResult(Object result)
 393  
             {
 394  2
                 value = result;
 395  
 
 396  2
                 return true;
 397  
             }
 398  
         };
 399  
 
 400  2
         resources.triggerContextEvent(EventConstants.ADD_ROW, context, callback);
 401  
 
 402  2
         if (value == null)
 403  0
             throw new IllegalArgumentException(
 404  
                     String.format("Event handler for event 'addRow' from %s should have returned a non-null value.",
 405  
                                   resources.getCompleteId()));
 406  
 
 407  
 
 408  2
         renderingInjector = true;
 409  
 
 410  2
         pageRenderQueue.addPartialMarkupRendererFilter(new PartialMarkupRendererFilter()
 411  
         {
 412  2
             public void renderMarkup(MarkupWriter writer, JSONObject reply, PartialMarkupRenderer renderer)
 413  
             {
 414  2
                 pushContext();
 415  
 
 416  2
                 renderer.renderMarkup(writer, reply);
 417  
 
 418  2
                 popContext();
 419  2
             }
 420  
         });
 421  
 
 422  2
         return ajaxResponse;
 423  
     }
 424  
 
 425  
     @Log
 426  
     Object onTriggerRemoveRow(String rowId)
 427  
     {
 428  2
         Object value = encoder.toValue(rowId);
 429  
 
 430  2
         resources.triggerEvent(EventConstants.REMOVE_ROW, new Object[] { value }, null);
 431  
 
 432  2
         return new JSONObject();
 433  
     }
 434  
 
 435  
     private void beginHeartbeat()
 436  
     {
 437  4
         heartbeat.begin();
 438  4
     }
 439  
 
 440  
     private void endHeartbeat()
 441  
     {
 442  4
         heartbeat.end();
 443  4
     }
 444  
 }