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    }