001 // Copyright May 20, 2006 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 package org.apache.tapestry.services.impl; 015 016 import org.apache.hivemind.ClassResolver; 017 import org.apache.hivemind.PoolManageable; 018 import org.apache.hivemind.Resource; 019 import org.apache.hivemind.util.ClasspathResource; 020 import org.apache.tapestry.*; 021 import org.apache.tapestry.dojo.IWidget; 022 import org.apache.tapestry.engine.DirectEventServiceParameter; 023 import org.apache.tapestry.engine.IEngineService; 024 import org.apache.tapestry.engine.IScriptSource; 025 import org.apache.tapestry.html.Body; 026 import org.apache.tapestry.internal.Component; 027 import org.apache.tapestry.internal.event.ComponentEventProperty; 028 import org.apache.tapestry.internal.event.EventBoundListener; 029 import org.apache.tapestry.internal.event.IComponentEventInvoker; 030 import org.apache.tapestry.services.ComponentRenderWorker; 031 import org.apache.tapestry.util.ScriptUtils; 032 033 import java.util.*; 034 035 036 /** 037 * Implementation that handles connecting events to listener 038 * method invocations. 039 * 040 * @author jkuhnert 041 */ 042 public class ComponentEventConnectionWorker implements ComponentRenderWorker, PoolManageable 043 { 044 /** Stored in {@link IRequestCycle} with associated forms. */ 045 public static final String FORM_NAME_LIST = "org.apache.tapestry.services.impl.ComponentEventConnectionFormNames-"; 046 047 // holds mapped event listener info 048 private IComponentEventInvoker _invoker; 049 050 // generates links for scripts 051 private IEngineService _eventEngine; 052 053 // handles resolving and loading different component event 054 // connection script types 055 private IScriptSource _scriptSource; 056 057 // script path references 058 private String _componentScript; 059 private String _widgetScript; 060 private String _elementScript; 061 062 // resolves classpath relative resources 063 private ClassResolver _resolver; 064 065 // wrappers around resolved script templates 066 private ClasspathResource _componentResource; 067 private ClasspathResource _widgetResource; 068 private ClasspathResource _elementResource; 069 070 /** 071 * For event connections referencing forms that have not been rendered yet. 072 */ 073 private Map _deferredFormConnections = new HashMap(24); 074 075 /** 076 * Used to store deferred form connection information, but most importantly is used 077 * to provide unique equals/hashcode semantics. 078 */ 079 class DeferredFormConnection { 080 081 String _formId; 082 Map _scriptParms; 083 Boolean _async; 084 Boolean _validate; 085 String _uniqueHash; 086 087 public DeferredFormConnection(String formId, Map scriptParms, Boolean async, 088 Boolean validate, String uniqueHash) 089 { 090 _formId = formId; 091 _scriptParms = scriptParms; 092 _async = async; 093 _validate = validate; 094 _uniqueHash = uniqueHash; 095 } 096 097 public boolean equals(Object o) 098 { 099 if (this == o) return true; 100 if (o == null || getClass() != o.getClass()) return false; 101 102 DeferredFormConnection that = (DeferredFormConnection) o; 103 104 if (_uniqueHash != null ? !_uniqueHash.equals(that._uniqueHash) : that._uniqueHash != null) 105 return false; 106 107 return true; 108 } 109 110 public int hashCode() 111 { 112 return (_uniqueHash != null ? _uniqueHash.hashCode() : 0); 113 } 114 } 115 116 public void activateService() 117 { 118 _deferredFormConnections.clear(); 119 } 120 121 public void passivateService() 122 { 123 } 124 125 /** 126 * {@inheritDoc} 127 */ 128 public void renderComponent(IRequestCycle cycle, IComponent component) 129 { 130 if (cycle.isRewinding()) 131 return; 132 133 if (Component.class.isInstance(component) && !((Component)component).hasEvents() && !IForm.class.isInstance(component)) 134 return; 135 136 if (TapestryUtils.getOptionalPageRenderSupport(cycle) == null) 137 return; 138 139 // Don't render fields being pre-rendered, otherwise we'll render twice 140 IComponent field = (IComponent)cycle.getAttribute(TapestryUtils.FIELD_PRERENDER); 141 if (field != null && field == component) 142 return; 143 144 linkComponentEvents(cycle, component); 145 146 linkElementEvents(cycle, component); 147 148 if (IForm.class.isInstance(component)) 149 mapFormNames(cycle, (IForm)component); 150 151 if (isDeferredForm(component)) 152 linkDeferredForm(cycle, (IForm)component); 153 } 154 155 void linkComponentEvents(IRequestCycle cycle, IComponent component) 156 { 157 ComponentEventProperty[] props = _invoker.getEventPropertyListeners(component.getExtendedId()); 158 if (props == null) 159 return; 160 161 for (int i=0; i < props.length; i++) 162 { 163 String clientId = component.getClientId(); 164 165 Map parms = new HashMap(); 166 parms.put("clientId", clientId); 167 parms.put("component", component); 168 169 Object[][] events = getEvents(props[i], clientId); 170 Object[][] formEvents = filterFormEvents(props[i], parms, cycle); 171 172 if (events.length < 1 && formEvents.length < 1) 173 continue; 174 175 DirectEventServiceParameter dsp = 176 new DirectEventServiceParameter((IDirectEvent)component, new Object[] {}, new String[] {}, false); 177 178 parms.put("url", _eventEngine.getLink(false, dsp).getURL()); 179 parms.put("events", events); 180 parms.put("formEvents", formEvents); 181 182 PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, component); 183 Resource resource = getScript(component); 184 185 _scriptSource.getScript(resource).execute(component, cycle, prs, parms); 186 } 187 } 188 189 void linkElementEvents(IRequestCycle cycle, IComponent component) 190 { 191 if (!component.getSpecification().hasElementEvents()) 192 return; 193 194 DirectEventServiceParameter dsp = 195 new DirectEventServiceParameter((IDirectEvent)component, new Object[] {}, new String[] {}, false); 196 197 String url = _eventEngine.getLink(false, dsp).getURL(); 198 199 PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, component); 200 Resource resource = getElementScript(); 201 202 Map elements = component.getSpecification().getElementEvents(); 203 Iterator keys = elements.keySet().iterator(); 204 205 // build our list of targets / events 206 while (keys.hasNext()) 207 { 208 Map parms = new HashMap(); 209 210 String target = (String)keys.next(); 211 212 ComponentEventProperty prop = (ComponentEventProperty)elements.get(target); 213 214 parms.put("component", component); 215 parms.put("target", target); 216 parms.put("url", url); 217 parms.put("events", getEvents(prop, target)); 218 parms.put("formEvents", filterFormEvents(prop, parms, cycle)); 219 220 _scriptSource.getScript(resource).execute(component, cycle, prs, parms); 221 } 222 } 223 224 /** 225 * {@inheritDoc} 226 */ 227 public void renderBody(IRequestCycle cycle, Body component) 228 { 229 if (cycle.isRewinding()) 230 return; 231 232 renderComponent(cycle, component); 233 234 // just in case 235 _deferredFormConnections.clear(); 236 } 237 238 void mapFormNames(IRequestCycle cycle, IForm form) 239 { 240 List names = (List)cycle.getAttribute(FORM_NAME_LIST + form.getExtendedId()); 241 242 if (names == null) 243 { 244 names = new ArrayList(); 245 cycle.setAttribute(FORM_NAME_LIST + form.getExtendedId(), names); 246 } 247 248 names.add(form.getName()); 249 } 250 251 void linkDeferredForm(IRequestCycle cycle, IForm form) 252 { 253 List deferred = (List)_deferredFormConnections.remove(form.getExtendedId()); 254 255 for (int i=0; i < deferred.size(); i++) 256 { 257 DeferredFormConnection fConn = (DeferredFormConnection)deferred.get(i); 258 Map scriptParms = fConn._scriptParms; 259 260 // don't want any events accidently connected again 261 scriptParms.remove("events"); 262 263 IComponent component = (IComponent)scriptParms.get("component"); 264 265 // fire off element based events first 266 267 linkElementEvents(cycle, component); 268 269 ComponentEventProperty[] props = _invoker.getEventPropertyListeners(component.getExtendedId()); 270 if (props == null) 271 continue; 272 273 for (int e=0; e < props.length; e++) 274 { 275 Object[][] formEvents = buildFormEvents(cycle, form.getExtendedId(), 276 props[e].getFormEvents(), fConn._async, 277 fConn._validate, fConn._uniqueHash); 278 279 scriptParms.put("formEvents", formEvents); 280 281 // execute script 282 283 PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, component); 284 Resource resource = getScript(component); 285 286 _scriptSource.getScript(resource).execute(form, cycle, prs, scriptParms); 287 } 288 } 289 } 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 Set events = prop.getEvents(); 301 List ret = new ArrayList(); 302 303 Iterator it = events.iterator(); 304 while (it.hasNext()) 305 { 306 String event = (String)it.next(); 307 308 int hash = 0; 309 List listeners = prop.getEventListeners(event); 310 311 for (int i=0; i < listeners.size(); i++) 312 hash += listeners.get(i).hashCode(); 313 314 ret.add(new Object[]{ event, ScriptUtils.functionHash(event + hash + clientId) }); 315 } 316 317 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 List formNames = (List)cycle.getAttribute(FORM_NAME_LIST + formId); 324 List retval = new ArrayList(); 325 326 Iterator it = events.iterator(); 327 328 while (it.hasNext()) 329 { 330 String event = (String)it.next(); 331 332 retval.add(new Object[]{event, formNames, async, validate, 333 ScriptUtils.functionHash(new String(uniqueHash + event)) }); 334 } 335 336 return (Object[][])retval.toArray(new Object[retval.size()][5]); 337 } 338 339 Resource getScript(IComponent component) 340 { 341 if (IWidget.class.isInstance(component)) { 342 343 if (_widgetResource == null) 344 _widgetResource = new ClasspathResource(_resolver, _widgetScript); 345 346 return _widgetResource; 347 } 348 349 if (_componentResource == null) 350 _componentResource = new ClasspathResource(_resolver, _componentScript); 351 352 return _componentResource; 353 } 354 355 Resource getElementScript() 356 { 357 if (_elementResource == null) 358 _elementResource = new ClasspathResource(_resolver, _elementScript); 359 360 return _elementResource; 361 } 362 363 boolean isDeferredForm(IComponent component) 364 { 365 if (IForm.class.isInstance(component) 366 && _deferredFormConnections.get(component.getExtendedId()) != null) 367 return true; 368 369 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 Set events = prop.getFormEvents(); 390 391 if (events.size() < 1) 392 return new Object[0][0]; 393 394 List retval = new ArrayList(); 395 396 Iterator it = events.iterator(); 397 while (it.hasNext()) 398 { 399 String event = (String)it.next(); 400 Iterator lit = prop.getFormEventListeners(event).iterator(); 401 402 while (lit.hasNext()) 403 { 404 EventBoundListener listener = (EventBoundListener)lit.next(); 405 406 String formId = listener.getFormId(); 407 List formNames = (List)cycle.getAttribute(FORM_NAME_LIST + formId); 408 409 // defer connection until form is rendered 410 if (formNames == null) 411 { 412 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 break; 420 } 421 422 // form has been rendered so go ahead 423 retval.add(new Object[] { 424 event, formNames, 425 Boolean.valueOf(listener.isAsync()), 426 Boolean.valueOf(listener.isValidateForm()), 427 ScriptUtils.functionHash(listener) 428 }); 429 } 430 } 431 432 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 List deferred = (List)_deferredFormConnections.get(formId); 458 if (deferred == null) 459 { 460 deferred = new ArrayList(); 461 _deferredFormConnections.put(formId, deferred); 462 } 463 464 DeferredFormConnection connection = new DeferredFormConnection(formId, scriptParms, Boolean.valueOf(async), 465 Boolean.valueOf(validate), uniqueHash); 466 467 if (!deferred.contains(connection)) 468 deferred.add(connection); 469 } 470 471 // for testing 472 Map getDefferedFormConnections() 473 { 474 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 _invoker = invoker; 484 } 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 _eventEngine = eventEngine; 495 } 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 _componentScript = script; 505 } 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 _widgetScript = script; 515 } 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 _elementScript = script; 525 } 526 527 /** 528 * The service that parses script files. 529 * @param scriptSource Service. 530 */ 531 public void setScriptSource(IScriptSource scriptSource) 532 { 533 _scriptSource = scriptSource; 534 } 535 536 public void setClassResolver(ClassResolver resolver) 537 { 538 _resolver = resolver; 539 } 540 }