Coverage Report - org.apache.tapestry.services.impl.ComponentEventConnectionWorker
 
Classes in this File Line Coverage Branch Coverage Complexity
ComponentEventConnectionWorker
92% 
94% 
2.769
 
 1  
 // Copyright May 20, 2006 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  
 package org.apache.tapestry.services.impl;
 15  
 
 16  
 import org.apache.hivemind.ClassResolver;
 17  
 import org.apache.hivemind.PoolManageable;
 18  
 import org.apache.hivemind.Resource;
 19  
 import org.apache.hivemind.util.ClasspathResource;
 20  
 import org.apache.tapestry.*;
 21  
 import org.apache.tapestry.dojo.IWidget;
 22  
 import org.apache.tapestry.engine.DirectEventServiceParameter;
 23  
 import org.apache.tapestry.engine.IEngineService;
 24  
 import org.apache.tapestry.engine.IScriptSource;
 25  
 import org.apache.tapestry.html.Body;
 26  
 import org.apache.tapestry.internal.Component;
 27  
 import org.apache.tapestry.internal.event.ComponentEventProperty;
 28  
 import org.apache.tapestry.internal.event.EventBoundListener;
 29  
 import org.apache.tapestry.internal.event.IComponentEventInvoker;
 30  
 import org.apache.tapestry.services.ComponentRenderWorker;
 31  
 import org.apache.tapestry.util.ScriptUtils;
 32  
 
 33  
 import java.util.*;
 34  
 
 35  
 
 36  
 /**
 37  
  * Implementation that handles connecting events to listener
 38  
  * method invocations.
 39  
  *
 40  
  * @author jkuhnert
 41  
  */
 42  7
 public class ComponentEventConnectionWorker implements ComponentRenderWorker, PoolManageable
 43  
 {
 44  
     /** Stored in {@link IRequestCycle} with associated forms. */
 45  
     public static final String FORM_NAME_LIST =  "org.apache.tapestry.services.impl.ComponentEventConnectionFormNames-";
 46  
 
 47  
     // holds mapped event listener info
 48  
     private IComponentEventInvoker _invoker;
 49  
 
 50  
     // generates links for scripts
 51  
     private IEngineService _eventEngine;
 52  
 
 53  
     // handles resolving and loading different component event 
 54  
     // connection script types
 55  
     private IScriptSource _scriptSource;
 56  
 
 57  
     // script path references
 58  
     private String _componentScript;
 59  
     private String _widgetScript;
 60  
     private String _elementScript;
 61  
 
 62  
     // resolves classpath relative resources
 63  
     private ClassResolver _resolver;
 64  
 
 65  
     // wrappers around resolved script templates
 66  
     private ClasspathResource _componentResource;
 67  
     private ClasspathResource _widgetResource;
 68  
     private ClasspathResource _elementResource;
 69  
 
 70  
     /**
 71  
      * For event connections referencing forms that have not been rendered yet.
 72  
      */
 73  7
     private Map _deferredFormConnections = new HashMap(24);
 74  
 
 75  
     /**
 76  
      * Used to store deferred form connection information, but most importantly is used
 77  
      * to provide unique equals/hashcode semantics.
 78  
      */
 79  7
     class DeferredFormConnection {
 80  
 
 81  
         String _formId;
 82  
         Map _scriptParms;
 83  
         Boolean _async;
 84  
         Boolean _validate;
 85  
         String _uniqueHash;
 86  
 
 87  
         public DeferredFormConnection(String formId, Map scriptParms, Boolean async,
 88  
                                       Boolean validate, String uniqueHash)
 89  6
         {
 90  6
             _formId = formId;
 91  6
             _scriptParms = scriptParms;
 92  6
             _async = async;
 93  6
             _validate = validate;
 94  6
             _uniqueHash = uniqueHash;
 95  6
         }
 96  
 
 97  
         public boolean equals(Object o)
 98  
         {
 99  5
             if (this == o) return true;
 100  5
             if (o == null || getClass() != o.getClass()) return false;
 101  
 
 102  5
             DeferredFormConnection that = (DeferredFormConnection) o;
 103  
 
 104  5
             if (_uniqueHash != null ? !_uniqueHash.equals(that._uniqueHash) : that._uniqueHash != null) return false;
 105  
 
 106  1
             return true;
 107  
         }
 108  
 
 109  
         public int hashCode()
 110  
         {
 111  0
             return (_uniqueHash != null ? _uniqueHash.hashCode() : 0);
 112  
         }
 113  
     }
 114  
 
 115  
     public void activateService()
 116  
     {
 117  0
         _deferredFormConnections.clear();
 118  0
     }
 119  
 
 120  
     public void passivateService()
 121  
     {
 122  0
     }
 123  
 
 124  
     /**
 125  
      * {@inheritDoc}
 126  
      */
 127  
     public void renderComponent(IRequestCycle cycle, IComponent component)
 128  
     {
 129  13
         if (cycle.isRewinding())
 130  1
             return;
 131  
 
 132  15
         if (Component.class.isInstance(component) && !((Component)component).hasEvents() && !IForm.class.isInstance(component))
 133  0
             return;
 134  
 
 135  12
         if (TapestryUtils.getOptionalPageRenderSupport(cycle) == null)
 136  1
             return;
 137  
 
 138  
         // Don't render fields being pre-rendered, otherwise we'll render twice
 139  11
         IComponent field = (IComponent)cycle.getAttribute(TapestryUtils.FIELD_PRERENDER);
 140  11
         if (field != null && field == component)
 141  1
             return;
 142  
 
 143  10
         linkComponentEvents(cycle, component);
 144  
 
 145  10
         linkElementEvents(cycle, component);
 146  
 
 147  10
         if (IForm.class.isInstance(component))
 148  1
             mapFormNames(cycle, (IForm)component);
 149  
 
 150  10
         if (isDeferredForm(component))
 151  1
             linkDeferredForm(cycle, (IForm)component);
 152  10
     }
 153  
 
 154  
     void linkComponentEvents(IRequestCycle cycle, IComponent component)
 155  
     {
 156  10
         ComponentEventProperty[] props = _invoker.getEventPropertyListeners(component.getExtendedId());
 157  10
         if (props == null)
 158  0
             return;
 159  
 
 160  17
         for (int i=0; i < props.length; i++) {
 161  
 
 162  7
             String clientId = component.getClientId();
 163  
 
 164  7
             Map parms = new HashMap();
 165  7
             parms.put("clientId", clientId);
 166  7
             parms.put("component", component);
 167  
 
 168  7
             Object[][] events = getEvents(props[i], clientId);
 169  7
             Object[][] formEvents = filterFormEvents(props[i], parms, cycle);
 170  
 
 171  7
             if (events.length < 1 && formEvents.length < 1)
 172  5
                 continue;
 173  
 
 174  2
             DirectEventServiceParameter dsp =
 175  
               new DirectEventServiceParameter((IDirectEvent)component, new Object[] {}, new String[] {}, false);
 176  
 
 177  2
             parms.put("url", _eventEngine.getLink(false, dsp).getURL());
 178  2
             parms.put("events", events);
 179  2
             parms.put("formEvents", formEvents);
 180  
 
 181  2
             PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, component);
 182  2
             Resource resource = getScript(component);
 183  
 
 184  2
             _scriptSource.getScript(resource).execute(component, cycle, prs, parms);
 185  
         }
 186  10
     }
 187  
 
 188  
     void linkElementEvents(IRequestCycle cycle, IComponent component)
 189  
     {
 190  12
         if (!component.getSpecification().hasElementEvents())
 191  11
             return;
 192  
 
 193  1
         DirectEventServiceParameter dsp =
 194  
           new DirectEventServiceParameter((IDirectEvent)component, new Object[] {}, new String[] {}, false);
 195  
 
 196  1
         String url = _eventEngine.getLink(false, dsp).getURL();
 197  
 
 198  1
         PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, component);
 199  1
         Resource resource = getElementScript();
 200  
 
 201  1
         Map elements = component.getSpecification().getElementEvents();
 202  1
         Iterator keys = elements.keySet().iterator();
 203  
 
 204  
         // build our list of targets / events
 205  2
         while (keys.hasNext()) {
 206  
 
 207  1
             Map parms = new HashMap();
 208  
 
 209  1
             String target = (String)keys.next();
 210  
 
 211  1
             ComponentEventProperty prop = (ComponentEventProperty)elements.get(target);
 212  
 
 213  1
             parms.put("component", component);
 214  1
             parms.put("target", target);
 215  1
             parms.put("url", url);
 216  1
             parms.put("events", getEvents(prop, target));
 217  1
             parms.put("formEvents", filterFormEvents(prop, parms, cycle));
 218  
 
 219  1
             _scriptSource.getScript(resource).execute(component, cycle, prs, parms);
 220  1
         }
 221  1
     }
 222  
 
 223  
     /**
 224  
      * {@inheritDoc}
 225  
      */
 226  
     public void renderBody(IRequestCycle cycle, Body component)
 227  
     {
 228  0
         if (cycle.isRewinding())
 229  0
             return;
 230  
 
 231  0
         renderComponent(cycle, component);
 232  
 
 233  
         // just in case
 234  0
         _deferredFormConnections.clear();
 235  0
     }
 236  
 
 237  
     void mapFormNames(IRequestCycle cycle, IForm form)
 238  
     {
 239  1
         List names = (List)cycle.getAttribute(FORM_NAME_LIST + form.getExtendedId());
 240  
 
 241  1
         if (names == null) {
 242  1
             names = new ArrayList();
 243  
 
 244  1
             cycle.setAttribute(FORM_NAME_LIST + form.getExtendedId(), names);
 245  
         }
 246  
 
 247  1
         names.add(form.getName());
 248  1
     }
 249  
 
 250  
     void linkDeferredForm(IRequestCycle cycle, IForm form)
 251  
     {
 252  1
         List deferred = (List)_deferredFormConnections.remove(form.getExtendedId());
 253  
 
 254  3
         for (int i=0; i < deferred.size(); i++)
 255  
         {
 256  2
             DeferredFormConnection fConn = (DeferredFormConnection)deferred.get(i);
 257  2
             Map scriptParms = fConn._scriptParms;
 258  
 
 259  
             // don't want any events accidently connected again
 260  2
             scriptParms.remove("events");
 261  
 
 262  2
             IComponent component = (IComponent)scriptParms.get("component");
 263  
 
 264  
             // fire off element based events first
 265  
 
 266  2
             linkElementEvents(cycle, component);
 267  
 
 268  2
             ComponentEventProperty[] props = _invoker.getEventPropertyListeners(component.getExtendedId());
 269  2
             if (props == null)
 270  0
                 continue;
 271  
 
 272  4
             for (int e=0; e < props.length; e++) {
 273  
 
 274  2
                 Object[][] formEvents = buildFormEvents(cycle, form.getExtendedId(),
 275  
                                                         props[e].getFormEvents(), fConn._async,
 276  
                                                         fConn._validate, fConn._uniqueHash);
 277  
 
 278  2
                 scriptParms.put("formEvents", formEvents);
 279  
 
 280  
                 // execute script
 281  
 
 282  2
                 PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, component);
 283  2
                 Resource resource = getScript(component);
 284  
 
 285  2
                 _scriptSource.getScript(resource).execute(form, cycle, prs, scriptParms);
 286  
             }
 287  
         }
 288  1
     }
 289  
 
 290  
     /**
 291  
      * Generates a two dimensional array containing the event name in the first
 292  
      * index and a unique hashcode for the event binding in the second.
 293  
      *
 294  
      * @param prop The component event properties object the events are managed in.
 295  
      * @return A two dimensional array containing all events, or empty array if none exist.
 296  
      */
 297  
     Object[][] getEvents(ComponentEventProperty prop, String clientId)
 298  
     {
 299  9
         Set events = prop.getEvents();
 300  9
         List ret = new ArrayList();
 301  
 
 302  9
         Iterator it = events.iterator();
 303  13
         while (it.hasNext())
 304  
         {
 305  4
             String event = (String)it.next();
 306  
 
 307  4
             int hash = 0;
 308  4
             List listeners = prop.getEventListeners(event);
 309  
 
 310  8
             for (int i=0; i < listeners.size(); i++)
 311  4
                 hash += listeners.get(i).hashCode();
 312  
 
 313  4
             ret.add(new Object[]{ event, ScriptUtils.functionHash(event + hash + clientId) });
 314  4
         }
 315  
 
 316  9
         return (Object[][])ret.toArray(new Object[ret.size()][2]);
 317  
     }
 318  
 
 319  
     Object[][] buildFormEvents(IRequestCycle cycle, String formId, Set events,
 320  
                                Boolean async, Boolean validate, Object uniqueHash)
 321  
     {
 322  2
         List formNames = (List)cycle.getAttribute(FORM_NAME_LIST + formId);
 323  2
         List retval = new ArrayList();
 324  
 
 325  2
         Iterator it = events.iterator();
 326  
 
 327  4
         while (it.hasNext())
 328  
         {
 329  2
             String event = (String)it.next();
 330  
 
 331  2
             retval.add(new Object[]{event, formNames, async, validate,
 332  
                                     ScriptUtils.functionHash(new String(uniqueHash + event)) });
 333  2
         }
 334  
 
 335  2
         return (Object[][])retval.toArray(new Object[retval.size()][5]);
 336  
     }
 337  
 
 338  
     Resource getScript(IComponent component)
 339  
     {
 340  14
         if (IWidget.class.isInstance(component)) {
 341  
 
 342  5
             if (_widgetResource == null)
 343  2
                 _widgetResource = new ClasspathResource(_resolver, _widgetScript);
 344  
 
 345  5
             return _widgetResource;
 346  
         }
 347  
 
 348  9
         if (_componentResource == null)
 349  3
             _componentResource = new ClasspathResource(_resolver, _componentScript);
 350  
 
 351  9
         return _componentResource;
 352  
     }
 353  
 
 354  
     Resource getElementScript()
 355  
     {
 356  3
         if (_elementResource == null)
 357  1
             _elementResource = new ClasspathResource(_resolver, _elementScript);
 358  
 
 359  3
         return _elementResource;
 360  
     }
 361  
 
 362  
     boolean isDeferredForm(IComponent component)
 363  
     {
 364  10
         if (IForm.class.isInstance(component)
 365  
             && _deferredFormConnections.get(((IForm)component).getExtendedId()) != null)
 366  1
             return true;
 367  
 
 368  9
         return false;
 369  
     }
 370  
 
 371  
     /**
 372  
      * For each form event attempts to find a rendered form name list that corresponds
 373  
      * to the actual client ids that the form can be connected to. If the form hasn't been
 374  
      * rendered yet the events will be filtered out and deferred for execution <i>after</i>
 375  
      * the form has rendererd.
 376  
      *
 377  
      * @param prop
 378  
      *          The configured event properties.
 379  
      * @param scriptParms
 380  
      *          The parameters to eventually be passed in to the javascript tempate.
 381  
      * @param cycle
 382  
      *          The current cycle.
 383  
      *
 384  
      * @return A set of events that can be connected now because the form has already rendered.
 385  
      */
 386  
     Object[][] filterFormEvents(ComponentEventProperty prop, Map scriptParms, IRequestCycle cycle)
 387  
     {
 388  8
         Set events = prop.getFormEvents();
 389  
 
 390  8
         if (events.size() < 1)
 391  3
             return new Object[0][0];
 392  
 
 393  5
         List retval = new ArrayList();
 394  
 
 395  5
         Iterator it = events.iterator();
 396  11
         while (it.hasNext())
 397  
         {
 398  6
             String event = (String)it.next();
 399  6
             Iterator lit = prop.getFormEventListeners(event).iterator();
 400  
 
 401  6
             while (lit.hasNext())
 402  
             {
 403  6
                 EventBoundListener listener = (EventBoundListener)lit.next();
 404  
 
 405  6
                 String formId = listener.getFormId();
 406  6
                 List formNames = (List)cycle.getAttribute(FORM_NAME_LIST + formId);
 407  
 
 408  
                 // defer connection until form is rendered
 409  6
                 if (formNames == null)
 410  
                 {
 411  6
                     deferFormConnection(formId, scriptParms,
 412  
                                         listener.isAsync(),
 413  
                                         listener.isValidateForm(),
 414  
                                         ScriptUtils.functionHash(listener.hashCode() + (String) scriptParms.get("clientId")));
 415  
                     
 416  
                     /*deferFormConnection(formId, scriptParms,
 417  
                                         listener.isAsync(),
 418  
                                         listener.isValidateForm(),
 419  
                                         ScriptUtils.functionHash(listener.hashCode() + (String) scriptParms.get("clientId")));*/
 420  
 
 421  
                     // re-looping over the same property -> event listener list would
 422  
                     // result in duplicate bindings so break out 
 423  6
                     break;
 424  
                 }
 425  
 
 426  
                 // form has been rendered so go ahead
 427  0
                 retval.add(new Object[] {
 428  
                   event, formNames,
 429  
                   Boolean.valueOf(listener.isAsync()),
 430  
                   Boolean.valueOf(listener.isValidateForm()),
 431  
                   ScriptUtils.functionHash(listener)
 432  
                 });
 433  0
             }
 434  6
         }
 435  
 
 436  5
         return (Object[][])retval.toArray(new Object[retval.size()][5]);
 437  
     }
 438  
 
 439  
     /**
 440  
      * Temporarily stores the data needed to perform script evaluations that
 441  
      * connect a component event to submitting a particular form that hasn't
 442  
      * been rendered yet. We can't reliably connect to a form until its name has
 443  
      * been set by a render, which could happen multiple times if it's in a list.
 444  
      *
 445  
      * <p>
 446  
      * The idea here is that when the form actually ~is~ rendered we will look for 
 447  
      * any pending deferred operations and run them while also clearing out our
 448  
      * deferred list.
 449  
      * </p>
 450  
      *
 451  
      * @param formId The form to defer event connection for.
 452  
      * @param scriptParms The initial map of parameters for the connection @Script component.
 453  
      * @param async Whether or not the action taken should be asynchronous.
 454  
      * @param validate Whether or not the form should have client side validation run befor submitting.
 455  
      * @param uniqueHash Represents a hashcode() value that will help make client side function name
 456  
      *                  unique.
 457  
      */
 458  
     void deferFormConnection(String formId, Map scriptParms,
 459  
                              boolean async, boolean validate, String uniqueHash)
 460  
     {
 461  6
         List deferred = (List)_deferredFormConnections.get(formId);
 462  6
         if (deferred == null)
 463  
         {
 464  2
             deferred = new ArrayList();
 465  2
             _deferredFormConnections.put(formId, deferred);
 466  
         }
 467  
 
 468  6
         DeferredFormConnection connection = new DeferredFormConnection(formId, scriptParms, Boolean.valueOf(async),
 469  
                                                                        Boolean.valueOf(validate), uniqueHash);
 470  
 
 471  6
         if (!deferred.contains(connection))
 472  5
             deferred.add(connection);
 473  6
     }
 474  
 
 475  
     // for testing
 476  
     Map getDefferedFormConnections()
 477  
     {
 478  5
         return _deferredFormConnections;
 479  
     }
 480  
 
 481  
     /**
 482  
      * Sets the invoker to use/manage event connections.
 483  
      * @param invoker Manages component event invocations.
 484  
      */
 485  
     public void setEventInvoker(IComponentEventInvoker invoker)
 486  
     {
 487  5
         _invoker = invoker;
 488  5
     }
 489  
 
 490  
     /**
 491  
      * Sets the engine service that will be used to construct callback
 492  
      * URL references to invoke the specified components event listener.
 493  
      *
 494  
      * @param eventEngine Engine used to create client side urls for updating things async.
 495  
      */
 496  
     public void setEventEngine(IEngineService eventEngine)
 497  
     {
 498  4
         _eventEngine = eventEngine;
 499  4
     }
 500  
 
 501  
     /**
 502  
      * The javascript that will be used to connect the component
 503  
      * to its configured events. (if any)
 504  
      * @param script The component script functions.
 505  
      */
 506  
     public void setComponentScript(String script)
 507  
     {
 508  3
         _componentScript = script;
 509  3
     }
 510  
 
 511  
     /**
 512  
      * The javascript that will be used to connect the widget component
 513  
      * to its configured events. (if any)
 514  
      * @param script The dojo widget based script.
 515  
      */
 516  
     public void setWidgetScript(String script)
 517  
     {
 518  2
         _widgetScript = script;
 519  2
     }
 520  
 
 521  
     /**
 522  
      * The javascript that connects html elements to direct
 523  
      * listener methods.
 524  
      * @param script Event element target scripts.
 525  
      */
 526  
     public void setElementScript(String script)
 527  
     {
 528  2
         _elementScript = script;
 529  2
     }
 530  
 
 531  
     /**
 532  
      * The service that parses script files.
 533  
      * @param scriptSource Service.
 534  
      */
 535  
     public void setScriptSource(IScriptSource scriptSource)
 536  
     {
 537  3
         _scriptSource = scriptSource;
 538  3
     }
 539  
 
 540  
     public void setClassResolver(ClassResolver resolver)
 541  
     {
 542  4
         _resolver = resolver;
 543  4
     }
 544  
 }