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