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 }