blob: 1a2226527655d36d4bcc844da8b88630f487c37c [file] [log] [blame]
// Copyright May 20, 2006 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry.services.impl;
import org.apache.hivemind.ClassResolver;
import org.apache.hivemind.PoolManageable;
import org.apache.hivemind.Resource;
import org.apache.hivemind.util.ClasspathResource;
import org.apache.tapestry.*;
import org.apache.tapestry.dojo.IWidget;
import org.apache.tapestry.engine.DirectEventServiceParameter;
import org.apache.tapestry.engine.IEngineService;
import org.apache.tapestry.engine.IScriptSource;
import org.apache.tapestry.html.Body;
import org.apache.tapestry.internal.Component;
import org.apache.tapestry.internal.event.ComponentEventProperty;
import org.apache.tapestry.internal.event.EventBoundListener;
import org.apache.tapestry.internal.event.IComponentEventInvoker;
import org.apache.tapestry.services.ComponentRenderWorker;
import org.apache.tapestry.util.ScriptUtils;
import java.util.*;
/**
* Implementation that handles connecting events to listener
* method invocations.
*
* @author jkuhnert
*/
public class ComponentEventConnectionWorker implements ComponentRenderWorker, PoolManageable
{
/** Stored in {@link IRequestCycle} with associated forms. */
public static final String FORM_NAME_LIST = "org.apache.tapestry.services.impl.ComponentEventConnectionFormNames-";
// holds mapped event listener info
private IComponentEventInvoker _invoker;
// generates links for scripts
private IEngineService _eventEngine;
// handles resolving and loading different component event
// connection script types
private IScriptSource _scriptSource;
// script path references
private String _componentScript;
private String _widgetScript;
private String _elementScript;
// resolves classpath relative resources
private ClassResolver _resolver;
// wrappers around resolved script templates
private ClasspathResource _componentResource;
private ClasspathResource _widgetResource;
private ClasspathResource _elementResource;
/**
* For event connections referencing forms that have not been rendered yet.
*/
private Map _deferredFormConnections = new HashMap(24);
/**
* Used to store deferred form connection information, but most importantly is used
* to provide unique equals/hashcode semantics.
*/
class DeferredFormConnection {
String _formId;
Map _scriptParms;
Boolean _async;
Boolean _validate;
String _uniqueHash;
public DeferredFormConnection(String formId, Map scriptParms, Boolean async,
Boolean validate, String uniqueHash)
{
_formId = formId;
_scriptParms = scriptParms;
_async = async;
_validate = validate;
_uniqueHash = uniqueHash;
}
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DeferredFormConnection that = (DeferredFormConnection) o;
if (_uniqueHash != null ? !_uniqueHash.equals(that._uniqueHash) : that._uniqueHash != null) return false;
return true;
}
public int hashCode()
{
return (_uniqueHash != null ? _uniqueHash.hashCode() : 0);
}
}
public void activateService()
{
_deferredFormConnections.clear();
}
public void passivateService()
{
}
/**
* {@inheritDoc}
*/
public void renderComponent(IRequestCycle cycle, IComponent component)
{
if (cycle.isRewinding())
return;
if (Component.class.isInstance(component) && !((Component)component).hasEvents() && !IForm.class.isInstance(component))
return;
if (TapestryUtils.getOptionalPageRenderSupport(cycle) == null)
return;
// Don't render fields being pre-rendered, otherwise we'll render twice
IComponent field = (IComponent)cycle.getAttribute(TapestryUtils.FIELD_PRERENDER);
if (field != null && field == component)
return;
linkComponentEvents(cycle, component);
linkElementEvents(cycle, component);
if (IForm.class.isInstance(component))
mapFormNames(cycle, (IForm)component);
if (isDeferredForm(component))
linkDeferredForm(cycle, (IForm)component);
}
void linkComponentEvents(IRequestCycle cycle, IComponent component)
{
ComponentEventProperty[] props = _invoker.getEventPropertyListeners(component.getExtendedId());
if (props == null)
return;
for (int i=0; i < props.length; i++) {
String clientId = component.getClientId();
Map parms = new HashMap();
parms.put("clientId", clientId);
parms.put("component", component);
Object[][] events = getEvents(props[i], clientId);
Object[][] formEvents = filterFormEvents(props[i], parms, cycle);
if (events.length < 1 && formEvents.length < 1)
continue;
DirectEventServiceParameter dsp =
new DirectEventServiceParameter((IDirectEvent)component, new Object[] {}, new String[] {}, false);
parms.put("url", _eventEngine.getLink(false, dsp).getURL());
parms.put("events", events);
parms.put("formEvents", formEvents);
PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, component);
Resource resource = getScript(component);
_scriptSource.getScript(resource).execute(component, cycle, prs, parms);
}
}
void linkElementEvents(IRequestCycle cycle, IComponent component)
{
if (!component.getSpecification().hasElementEvents())
return;
DirectEventServiceParameter dsp =
new DirectEventServiceParameter((IDirectEvent)component, new Object[] {}, new String[] {}, false);
String url = _eventEngine.getLink(false, dsp).getURL();
PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, component);
Resource resource = getElementScript();
Map elements = component.getSpecification().getElementEvents();
Iterator keys = elements.keySet().iterator();
// build our list of targets / events
while (keys.hasNext()) {
Map parms = new HashMap();
String target = (String)keys.next();
ComponentEventProperty prop = (ComponentEventProperty)elements.get(target);
parms.put("component", component);
parms.put("target", target);
parms.put("url", url);
parms.put("events", getEvents(prop, target));
parms.put("formEvents", filterFormEvents(prop, parms, cycle));
_scriptSource.getScript(resource).execute(component, cycle, prs, parms);
}
}
/**
* {@inheritDoc}
*/
public void renderBody(IRequestCycle cycle, Body component)
{
if (cycle.isRewinding())
return;
renderComponent(cycle, component);
// just in case
_deferredFormConnections.clear();
}
void mapFormNames(IRequestCycle cycle, IForm form)
{
List names = (List)cycle.getAttribute(FORM_NAME_LIST + form.getExtendedId());
if (names == null) {
names = new ArrayList();
cycle.setAttribute(FORM_NAME_LIST + form.getExtendedId(), names);
}
names.add(form.getName());
}
void linkDeferredForm(IRequestCycle cycle, IForm form)
{
List deferred = (List)_deferredFormConnections.remove(form.getExtendedId());
for (int i=0; i < deferred.size(); i++)
{
DeferredFormConnection fConn = (DeferredFormConnection)deferred.get(i);
Map scriptParms = fConn._scriptParms;
// don't want any events accidently connected again
scriptParms.remove("events");
IComponent component = (IComponent)scriptParms.get("component");
// fire off element based events first
linkElementEvents(cycle, component);
ComponentEventProperty[] props = _invoker.getEventPropertyListeners(component.getExtendedId());
if (props == null)
continue;
for (int e=0; e < props.length; e++) {
Object[][] formEvents = buildFormEvents(cycle, form.getExtendedId(),
props[e].getFormEvents(), fConn._async,
fConn._validate, fConn._uniqueHash);
scriptParms.put("formEvents", formEvents);
// execute script
PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, component);
Resource resource = getScript(component);
_scriptSource.getScript(resource).execute(form, cycle, prs, scriptParms);
}
}
}
/**
* Generates a two dimensional array containing the event name in the first
* index and a unique hashcode for the event binding in the second.
*
* @param prop The component event properties object the events are managed in.
* @return A two dimensional array containing all events, or empty array if none exist.
*/
Object[][] getEvents(ComponentEventProperty prop, String clientId)
{
Set events = prop.getEvents();
List ret = new ArrayList();
Iterator it = events.iterator();
while (it.hasNext())
{
String event = (String)it.next();
int hash = 0;
List listeners = prop.getEventListeners(event);
for (int i=0; i < listeners.size(); i++)
hash += listeners.get(i).hashCode();
ret.add(new Object[]{ event, ScriptUtils.functionHash(event + hash + clientId) });
}
return (Object[][])ret.toArray(new Object[ret.size()][2]);
}
Object[][] buildFormEvents(IRequestCycle cycle, String formId, Set events,
Boolean async, Boolean validate, Object uniqueHash)
{
List formNames = (List)cycle.getAttribute(FORM_NAME_LIST + formId);
List retval = new ArrayList();
Iterator it = events.iterator();
while (it.hasNext())
{
String event = (String)it.next();
retval.add(new Object[]{event, formNames, async, validate,
ScriptUtils.functionHash(new String(uniqueHash + event)) });
}
return (Object[][])retval.toArray(new Object[retval.size()][5]);
}
Resource getScript(IComponent component)
{
if (IWidget.class.isInstance(component)) {
if (_widgetResource == null)
_widgetResource = new ClasspathResource(_resolver, _widgetScript);
return _widgetResource;
}
if (_componentResource == null)
_componentResource = new ClasspathResource(_resolver, _componentScript);
return _componentResource;
}
Resource getElementScript()
{
if (_elementResource == null)
_elementResource = new ClasspathResource(_resolver, _elementScript);
return _elementResource;
}
boolean isDeferredForm(IComponent component)
{
if (IForm.class.isInstance(component)
&& _deferredFormConnections.get(((IForm)component).getExtendedId()) != null)
return true;
return false;
}
/**
* For each form event attempts to find a rendered form name list that corresponds
* to the actual client ids that the form can be connected to. If the form hasn't been
* rendered yet the events will be filtered out and deferred for execution <i>after</i>
* the form has rendererd.
*
* @param prop
* The configured event properties.
* @param scriptParms
* The parameters to eventually be passed in to the javascript tempate.
* @param cycle
* The current cycle.
*
* @return A set of events that can be connected now because the form has already rendered.
*/
Object[][] filterFormEvents(ComponentEventProperty prop, Map scriptParms, IRequestCycle cycle)
{
Set events = prop.getFormEvents();
if (events.size() < 1)
return new Object[0][0];
List retval = new ArrayList();
Iterator it = events.iterator();
while (it.hasNext())
{
String event = (String)it.next();
Iterator lit = prop.getFormEventListeners(event).iterator();
while (lit.hasNext())
{
EventBoundListener listener = (EventBoundListener)lit.next();
String formId = listener.getFormId();
List formNames = (List)cycle.getAttribute(FORM_NAME_LIST + formId);
// defer connection until form is rendered
if (formNames == null)
{
deferFormConnection(formId, scriptParms,
listener.isAsync(),
listener.isValidateForm(),
ScriptUtils.functionHash(listener.hashCode() + (String) scriptParms.get("clientId")));
/*deferFormConnection(formId, scriptParms,
listener.isAsync(),
listener.isValidateForm(),
ScriptUtils.functionHash(listener.hashCode() + (String) scriptParms.get("clientId")));*/
// re-looping over the same property -> event listener list would
// result in duplicate bindings so break out
break;
}
// form has been rendered so go ahead
retval.add(new Object[] {
event, formNames,
Boolean.valueOf(listener.isAsync()),
Boolean.valueOf(listener.isValidateForm()),
ScriptUtils.functionHash(listener)
});
}
}
return (Object[][])retval.toArray(new Object[retval.size()][5]);
}
/**
* Temporarily stores the data needed to perform script evaluations that
* connect a component event to submitting a particular form that hasn't
* been rendered yet. We can't reliably connect to a form until its name has
* been set by a render, which could happen multiple times if it's in a list.
*
* <p>
* The idea here is that when the form actually ~is~ rendered we will look for
* any pending deferred operations and run them while also clearing out our
* deferred list.
* </p>
*
* @param formId The form to defer event connection for.
* @param scriptParms The initial map of parameters for the connection @Script component.
* @param async Whether or not the action taken should be asynchronous.
* @param validate Whether or not the form should have client side validation run befor submitting.
* @param uniqueHash Represents a hashcode() value that will help make client side function name
* unique.
*/
void deferFormConnection(String formId, Map scriptParms,
boolean async, boolean validate, String uniqueHash)
{
List deferred = (List)_deferredFormConnections.get(formId);
if (deferred == null)
{
deferred = new ArrayList();
_deferredFormConnections.put(formId, deferred);
}
DeferredFormConnection connection = new DeferredFormConnection(formId, scriptParms, Boolean.valueOf(async),
Boolean.valueOf(validate), uniqueHash);
if (!deferred.contains(connection))
deferred.add(connection);
}
// for testing
Map getDefferedFormConnections()
{
return _deferredFormConnections;
}
/**
* Sets the invoker to use/manage event connections.
* @param invoker Manages component event invocations.
*/
public void setEventInvoker(IComponentEventInvoker invoker)
{
_invoker = invoker;
}
/**
* Sets the engine service that will be used to construct callback
* URL references to invoke the specified components event listener.
*
* @param eventEngine Engine used to create client side urls for updating things async.
*/
public void setEventEngine(IEngineService eventEngine)
{
_eventEngine = eventEngine;
}
/**
* The javascript that will be used to connect the component
* to its configured events. (if any)
* @param script The component script functions.
*/
public void setComponentScript(String script)
{
_componentScript = script;
}
/**
* The javascript that will be used to connect the widget component
* to its configured events. (if any)
* @param script The dojo widget based script.
*/
public void setWidgetScript(String script)
{
_widgetScript = script;
}
/**
* The javascript that connects html elements to direct
* listener methods.
* @param script Event element target scripts.
*/
public void setElementScript(String script)
{
_elementScript = script;
}
/**
* The service that parses script files.
* @param scriptSource Service.
*/
public void setScriptSource(IScriptSource scriptSource)
{
_scriptSource = scriptSource;
}
public void setClassResolver(ClassResolver resolver)
{
_resolver = resolver;
}
}