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