001    // Copyright May 16, 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 net.sf.tacos.components.dojo;
015    
016    import java.util.Collection;
017    import java.util.HashMap;
018    import java.util.Iterator;
019    import java.util.List;
020    import java.util.Map;
021    
022    import net.sf.tacos.binding.ICallbackFunction;
023    import net.sf.tacos.util.JSONMarkupWriter;
024    
025    import org.apache.tapestry.IActionListener;
026    import org.apache.tapestry.IBinding;
027    import org.apache.tapestry.IDirect;
028    import org.apache.tapestry.IMarkupWriter;
029    import org.apache.tapestry.IRequestCycle;
030    import org.apache.tapestry.IScript;
031    import org.apache.tapestry.Tapestry;
032    import org.apache.tapestry.TapestryUtils;
033    import org.apache.tapestry.dojo.IWidget;
034    import org.apache.tapestry.engine.DirectServiceParameter;
035    import org.apache.tapestry.engine.IEngineService;
036    import org.apache.tapestry.engine.ILink;
037    import org.apache.tapestry.event.BrowserEvent;
038    import org.apache.tapestry.json.JSONObject;
039    import org.apache.tapestry.link.DirectLink;
040    import org.apache.tapestry.listener.ListenerInvoker;
041    
042    /**
043     * Implementation of any dojo widget as a tapestry component.
044     * The formal parameter <code>dojoType</code> is used to determine which dojo widget to create. 
045     * Informal parameters are used as javascript properties to create widget with.
046     * Please have a look into the <a href="http://dojotoolkit.org/api">http://dojotoolkit.org/api</a>
047     * for further information about the widget properties. A simple example is the dojo Clock widget: 
048     *<pre>
049     *&lt;div jwcid="@dojo:Widget" dojoType="Clock" timeZoneOffset="5" labelColor="#fff"/&gt;
050     *</pre>
051     * <p>
052     * The formal parameter <code>isContainer</code> allows a widget to have children.
053     * All the widgets within the body of a container are added as childs. For example dojo tabs:
054     * </p>
055     * <pre>
056     *&lt;div jwcid="@dojo:Widget" dojoType="TabContainer" isContainer="true"&gt;
057     *      &lt;div jwcid="@dojo:Widget" dojoType="ContentPane" label="Athens"&gt;Athens&lt;/div&gt;
058     *      &lt;div jwcid="@dojo:Widget" dojoType="ContentPane" label="Moscow"&gt;Moscow&lt;/div&gt;
059     *&lt;/div&gt;
060     *</pre>
061     * <p>
062     * DojoWidget allows you to connect any client side event to a listener method.
063     * Please see the dojo api for further information about the events fired by the particular widget.
064     * To connect a client side even just use the informal parameter with the corresponding name.
065     * For example, specifying a component definition such as:
066     * </p>
067     * <pre>
068     * &lt;div jwcid="@dojo:Widget" dojoType="..." onClick="listener:onClick" onMouseDown="listener:onMouseDown"/&gt;
069     * </pre>
070     * <p>
071     * It's possible to specify a list of events for which to generate non async requests specifying the dojo events
072     * list in the noAsync parameter as a comma separated list. This is particularly useful
073     * with menu/button widget that most of the times need normal requests to be generated.
074     * <pre>
075     * &lt;div jwcid="@dojo:Widget" dojoType="MenuItem2" onClick="listener:onMenuClick" noAsync="onClick"/&gt;
076     * </pre>
077     * </p>
078     * Furthermore it is poossible to connect a client side event to a javascript function.
079     * For this purpose use the prefix <code>callback</code>. For example:
080     * </p>
081     * <pre>
082     * &lt;div jwcid="@dojo:Widget" dojoType="SliderHorizontal" onValueChanged="callback:sliderCallback"/&gt;
083     * </pre>
084     *  <p>Whereby <code>sliderCallback</code> is a javascript function:</p>
085     * <pre>
086     *  function sliderCallback(value) {
087     *      alert(value);
088     *  }
089     * </pre>
090     * @since 4.1.0
091     */
092    public abstract class DojoWidget extends GenericWidget implements IDojoContainer, IDirect{
093    
094            public static final String CONTAINER_ATTRIBUTE = DojoWidget.class.getName();
095            
096        /**
097         * If the current component is a container.<br/>
098         * If the user has defined a value for 'isContainer', then that is used.
099         *
100         * Otherwise, several rules are used to determine if the component is
101         * a valid container. The rules are:<ul>
102         * <li>the dojoType parameter ends with 'Container'.</li>
103         * </ul>
104         *
105         */
106        public boolean getContainerState() {
107            if (isParameterBound("isContainer")) {
108                return getIsContainer();
109            } else {
110                String type = getDojoType();
111                return (type.endsWith("Container") || type.equals("ContentPane"));
112            }
113        }
114        
115        /**
116         * {@inheritDoc}
117         */
118        public void renderWidget(IMarkupWriter writer, IRequestCycle cycle){
119            boolean isContainer = getContainerState();
120            IDojoContainer previousContainer = (IDojoContainer) cycle.getAttribute(CONTAINER_ATTRIBUTE);
121            boolean rewinding = cycle.isRewinding();
122            
123            if (!rewinding) {
124                writer.begin(getTemplateTagName());
125                renderIdAttribute(writer, cycle);
126                String style = getStyle();
127                if (style!=null)
128                    writer.attribute("style", style);
129                
130                if(isContainer){
131                    cycle.setAttribute(CONTAINER_ATTRIBUTE, this);
132                }
133            }
134            
135            renderBody(writer, cycle);
136            
137            if (!rewinding) {
138                    writer.end();
139    
140                if (previousContainer!=null)
141                    previousContainer.addChild(this);            
142    
143                JSONMarkupWriter jsonWriter = new JSONMarkupWriter();
144                renderInformalParameters(jsonWriter, cycle);
145                JSONObject json = jsonWriter.getAttributes();
146                
147                Map asynclisteners = getAsyncListenerBindings();
148                Map listeners = getListenerBindings();
149                Map callbacks = getCallbackBindings();
150                removeListenersFromInputSymbols(json, asynclisteners);
151                removeListenersFromInputSymbols(json, listeners);
152                removeListenersFromInputSymbols(json, callbacks);
153    
154                Map parms = new HashMap();
155                String url = getLink().getURL();
156                parms.put("component", this);
157                parms.put("type", getDojoType());
158                parms.put("url", url);
159                parms.put("props", json.toString());
160                parms.put("asynclisteners", asynclisteners);
161                parms.put("listeners", listeners);
162                parms.put("callbacks", callbacks);
163                getScript().execute(this, cycle, TapestryUtils.getPageRenderSupport(cycle, this), parms);
164    
165                cycle.setAttribute(CONTAINER_ATTRIBUTE, previousContainer);
166            }
167        }
168        
169        
170        /**
171         * {@inheritDoc}
172         */
173            public void addChild(IWidget widget) {
174                // need to store the clientIds here instead of the widget instances,
175                // cause that's what's changing
176                    getChildren().add(widget.getClientId());
177            }
178            
179            /**
180             * The informal parameters of the component are used as arguments for <code>dojo.widget.Widget</code>.
181             * The method {@link this{@link #renderInformalParameters(IMarkupWriter, IRequestCycle)}}
182             * is used to pass through these parameters to the dojo widget. Some of the parameters does not
183             * represent dojo arguments and should not be passed through.
184             * This method can be used to remove specified bindings from the Map of input symbols.
185             * @param json json object to remove bindings from 
186             * @param bindings bindings to remove
187             */
188            private void removeListenersFromInputSymbols(JSONObject json, Map bindings){
189                    Iterator i = bindings.keySet().iterator();
190                    while(i.hasNext()){
191                            json.remove((String)i.next());
192                    }
193            }
194            
195            /**
196             * Returns a {@link Map} of the {@link org.apache.tapestry.IBinding bindings} implementing
197             * the {@link org.apache.tapestry.IActionListener} interface.
198             */
199            private Map getAllListenerBindings(){
200                    return getBindingsByClass(getBindingNames(), IActionListener.class);
201            }
202    
203            /**
204             * Returns a {@link Map} of the {@link org.apache.tapestry.IBinding bindings} implementing
205             * the {@link org.apache.tapestry.IActionListener} interface for which to
206             * generate non async requests.
207             */
208            public Map getListenerBindings(){
209                    Map listeners = getBindingsByClass(getBindingNames(), IActionListener.class);
210                    Map result = new HashMap();
211                    String[] noAsync = constructNoAsync();
212                    if (noAsync != null) {
213                            for (int i = 0; i < noAsync.length; i++) {
214                                    if (listeners.get(noAsync[i]) != null) {
215                                            result.put(noAsync[i], listeners.get(noAsync[i]));
216                                    }
217                            }               
218                    }
219                    return result;
220            }
221            /**
222             * Returns a {@link Map} of the {@link org.apache.tapestry.IBinding bindings} implementing
223             * the {@link org.apache.tapestry.IActionListener} interface for which
224             * to generate async requests.
225             */
226            public Map getAsyncListenerBindings(){
227                    Map listeners = getBindingsByClass(getBindingNames(), IActionListener.class);
228                    String[] noAsync = constructNoAsync();
229                    if (noAsync != null) {
230                            for (int i = 0; i < noAsync.length; i++) {
231                                    listeners.remove(noAsync[i]);
232                            }
233                    }
234                    return listeners;
235            }
236            
237            /**
238             * Returns an array containing the names of the events for which not to generate
239             * async requests
240             */
241            private String[] constructNoAsync() {
242                    String noAsync = getNoAsync();
243                    if (noAsync != null) {
244                            if (noAsync.indexOf(",") != -1) {
245                                    return noAsync.split(",");
246                            } else {
247                                    String[] result = {noAsync};
248                                    return result;
249                            }
250                    }
251                    return null;
252            }
253            
254            /**
255             * Returns a {@link Map} of the {@link org.apache.tapestry.IBinding bindings} implementing
256             * the {@link net.sf.tacos.binding.ICallbackFunction} interface.
257             */
258            public Map getCallbackBindings(){
259                    return getBindingsByClass(getBindingNames(), ICallbackFunction.class);
260            }
261            
262            /**
263             * Filters the {@link Map} of the {@link org.apache.tapestry.IBinding bindings} of the component 
264             * and returns a {@link Map} of {@link org.apache.tapestry.IBinding bindings} the specified Class parameter
265             * is assignable from.
266             * 
267             * @param names names of the bindings 
268             * @param clazz class to determine if it is assignable from the bindings
269             */
270            protected Map getBindingsByClass(Collection names, Class clazz){
271                    Map result = new HashMap();
272                    Iterator i = names.iterator();
273                    while(i.hasNext()){
274                            String name = (String)i.next();
275                            IBinding binding = getBinding(name);
276                            if(clazz.isAssignableFrom(binding.getClass())){
277                                    result.put(name, binding);
278                            }
279                    }
280                    return result;
281            }
282            
283            /**Returns a link*/
284            private ILink getLink(){
285                    Object[] parameters = DirectLink.constructServiceParameters(getParameters());
286                    DirectServiceParameter dsp = new DirectServiceParameter(this, parameters);
287                    return getEngine().getLink(isStateful(), dsp);
288            }
289            
290            /** Returns the url to use for a non async event  */
291            public String getNoAsyncEventUrl(String eventName){
292                    Object[] parameters = DirectLink.constructServiceParameters(getParameters());
293                    DirectServiceParameter dsp = new DirectServiceParameter(this, parameters);
294                    return getEngine().getLink(isStateful(), dsp).getURL() + "&" 
295                            + BrowserEvent.NAME + "=" + eventName;
296            }
297                    
298        /**
299         * Invoked by the direct service to trigger the application-specific action by notifying the
300         * {@link IActionListener listener}.
301         */
302            public void trigger(IRequestCycle cycle) {
303            String event = cycle.getParameter(BrowserEvent.NAME);
304            IActionListener listener = (IActionListener)getAllListenerBindings().get(event);
305            if (listener == null)
306                throw Tapestry.createRequiredParameterException(this, event);
307            getListenerInvoker().invokeListener(listener, this, cycle);
308                    
309            }
310    
311        /** Injected script. */
312        public abstract IScript getScript();
313        
314        /** Injected engine service . */
315        public abstract IEngineService getEngine();
316        
317        /** Injected listener invoker. */
318        public abstract ListenerInvoker getListenerInvoker();
319        
320        /** Returns the style. */
321        public abstract String getStyle();
322        
323        /** Returns the type of the dojo widget */
324        public abstract String getDojoType();
325        
326        /**Returns listener parameters*/
327        public abstract Object getParameters();
328        
329        /** Returns widget events for which to generate no async requests */
330        public abstract String getNoAsync();
331        
332        /**
333         * Returns a list of children widgetIds.
334         * The list is always empty if {@link #getIsContainer()} returns <code>false</code>.
335         */
336        public abstract List getChildren();
337    }