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    }