1   // ========================================================================
2   // Copyright 2008 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  //========================================================================
14  package org.mortbay.cometd;
15  
16  import java.lang.reflect.Method;
17  import java.util.Map;
18  import java.util.concurrent.ConcurrentHashMap;
19  
20  import org.mortbay.component.LifeCycle;
21  import org.mortbay.thread.QueuedThreadPool;
22  import org.mortbay.thread.ThreadPool;
23  
24  import dojox.cometd.Bayeux;
25  import dojox.cometd.Channel;
26  import dojox.cometd.Client;
27  import dojox.cometd.Listener;
28  import dojox.cometd.Message;
29  import dojox.cometd.MessageListener;
30  
31  /* ------------------------------------------------------------ */
32  /** Abstract Bayeux Service class.
33   * This is a base class to assist with the creation of server side {@ link Bayeux} 
34   * clients that provide services to remote Bayeux clients.   The class provides
35   * a Bayeux {@link Client} and {@link Listener} together with convenience methods to map
36   * subscriptions to methods on the derived class and to send responses to those methods.
37   * 
38   * <p>If a {@link #set_threadPool(ThreadPool)} is set, then messages are handled in their 
39   * own threads.  This is desirable if the handling of a message can take considerable time and 
40   * it is desired not to hold up the delivering thread (typically a HTTP request handling thread).
41   * 
42   * <p>If the BayeuxService is constructed asynchronously (the default), then messages are
43   * delivered unsynchronized and multiple simultaneous calls to handling methods may occur.
44   * 
45   * <p>If the BayeuxService is constructed as a synchronous service, then message delivery
46   * is synchronized on the internal {@link Client} instances used and only a single call will
47   * be made to the handler method (unless a thread pool is used).
48   *
49   * @see MessageListener
50   * @author gregw
51   *
52   */
53  public abstract class BayeuxService 
54  {
55      private String _name;
56      private Bayeux _bayeux;
57      private Client _client;
58      private Map<String,Method> _methods = new ConcurrentHashMap<String,Method>();
59      private ThreadPool _threadPool;
60      private MessageListener _listener;
61      
62      /* ------------------------------------------------------------ */
63      /** Instantiate the service.
64       * Typically the derived constructor will call {@ #subscribe(String, String)} to 
65       * map subscriptions to methods.
66       * @param bayeux The bayeux instance.
67       * @param name The name of the service (used as client ID prefix).
68       */
69      public BayeuxService(Bayeux bayeux,String name)
70      {
71          this(bayeux,name,0,false);
72      }
73  
74      /* ------------------------------------------------------------ */
75      /** Instantiate the service.
76       * Typically the derived constructor will call {@ #subscribe(String, String)} to 
77       * map subscriptions to methods.
78       * @param bayeux The bayeux instance.
79       * @param name The name of the service (used as client ID prefix).
80       * @param maxThreads The size of a ThreadPool to create to handle messages.
81       */
82      public BayeuxService(Bayeux bayeux,String name, int maxThreads)
83      {
84          this(bayeux,name,maxThreads,false);
85      }
86      
87      /* ------------------------------------------------------------ */
88      /** Instantiate the service.
89       * Typically the derived constructor will call {@ #subscribe(String, String)} to 
90       * map subscriptions to methods.
91       * @param bayeux The bayeux instance.
92       * @param name The name of the service (used as client ID prefix).
93       * @param maxThreads The size of a ThreadPool to create to handle messages.
94       * @param synchronous True if message delivery will be synchronized on the client.
95       */
96      public BayeuxService(Bayeux bayeux,String name, int maxThreads, boolean synchronous)
97      {
98          if (maxThreads>0)
99              setThreadPool(new QueuedThreadPool(maxThreads));
100         _name=name;
101         _bayeux=bayeux;
102         _client=_bayeux.newClient(name);  
103         if (synchronous)
104             _client.addListener(_listener=new SyncListen());
105         else
106             _client.addListener(_listener=new AsyncListen());
107     }
108 
109     /* ------------------------------------------------------------ */
110     public Bayeux getBayeux()
111     {
112         return _bayeux;
113     }
114 
115     /* ------------------------------------------------------------ */
116     public Client getClient()
117     {
118         return _client;
119     }
120 
121     /* ------------------------------------------------------------ */
122     public ThreadPool getThreadPool()
123     {
124         return _threadPool;
125     }
126 
127     /* ------------------------------------------------------------ */
128     /**
129      * Set the threadpool.
130      * If the {@link ThreadPool} is a {@link LifeCycle}, then it is started by this method.
131      * 
132      * @param pool 
133      */
134     public void setThreadPool(ThreadPool pool)
135     {
136         try
137         {
138             if (pool instanceof LifeCycle)
139                 if (!((LifeCycle)pool).isStarted())
140                     ((LifeCycle)pool).start();
141         }
142         catch(Exception e)
143         {
144             throw new IllegalStateException(e);
145         }
146         _threadPool = pool;
147     }
148     
149     /* ------------------------------------------------------------ */
150     /** Subscribe to a channel.
151      * Subscribe to channel and map a method to handle received messages.
152      * The method must have a unique name and one of the following signatures:<ul>
153      * <li><code>myMethod(Client fromClient,Object data)</code></li>
154      * <li><code>myMethod(Client fromClient,Object data,String id)</code></li>
155      * <li><code>myMethod(Client fromClient,String channel,Object data,String id)</code></li>
156      * </li>
157      * 
158      * The data parameter can be typed if
159      * the type of the data object published by the client is known (typically 
160      * Map<String,Object>). If the type of the data parameter is {@link Message} then
161      * the message object itself is passed rather than just the data.
162      * <p>
163      * Typically a service will subscribe to a channel in the "/service/**" space
164      * which is not a broadcast channel.  Messages published to these channels are
165      * only delivered to server side clients like this service.  
166      * 
167      * <p>Any object returned by a mapped subscription method is delivered to the 
168      * calling client and not broadcast. If the method returns void or null, then 
169      * no response is sent. A mapped subscription method may also call {@link #send(Client, String, Object, String)}
170      * to deliver a response message(s) to different clients and/or channels. It may
171      * also publish methods via the normal {@link Bayeux} API.
172      * <p>
173      * 
174      * 
175      * @param channelId The channel to subscribe to
176      * @param methodName The name of the method on this object to call when messages are recieved.
177      */
178     protected void subscribe(String channelId,String methodName)
179     {
180         Method method=null;
181         
182         Class<?> c=this.getClass();
183         while (c!=null && c!=Object.class)
184         {
185             Method[] methods = c.getDeclaredMethods();
186             for (int i=methods.length;i-->0;)
187             {
188                 if (methodName.equals(methods[i].getName()))
189                 {
190                     if (method!=null)
191                         throw new IllegalArgumentException("Multiple methods called '"+methodName+"'");
192                     method=methods[i];
193                 }
194             }
195             c=c.getSuperclass();
196         }
197         
198         if (method==null)
199             throw new NoSuchMethodError(methodName);
200         int params=method.getParameterTypes().length;
201         if (params<2 || params>4)
202             throw new IllegalArgumentException("Method '"+methodName+"' does not have 2or3 parameters");
203         if (!Client.class.isAssignableFrom(method.getParameterTypes()[0]))
204             throw new IllegalArgumentException("Method '"+methodName+"' does not have Client as first parameter");
205 
206         Channel channel=_bayeux.getChannel(channelId,true);
207 
208         if (((ChannelImpl)channel).getChannelId().isWild())
209         { 
210             final Method m=method;
211             Client wild_client=_bayeux.newClient(_name+"-wild");
212             wild_client.addListener(_listener instanceof MessageListener.Asynchronous?new AsyncWildListen(m):new SyncWildListen(m));
213             channel.subscribe(wild_client);
214         }
215         else
216         {
217             _methods.put(channelId,method);
218             channel.subscribe(_client);
219         }
220     }
221 
222     /* ------------------------------------------------------------ */
223     /** Send data to a individual client.
224      * The data passed is sent to the client as the "data" member of a message
225      * with the given channel and id.  The message is not published on the channel and is
226      * thus not broadcast to all channel subscribers.  However to the target client, the
227      * message appears as if it was broadcast.
228      * <p>
229      * Typcially this method is only required if a service method sends response(s) to 
230      * channels other than the subscribed channel. If the response is to be sent to the subscribed
231      * channel, then the data can simply be returned from the subscription method.
232      * 
233      * @param toClient The target client
234      * @param onChannel The channel the message is for
235      * @param data The data of the message
236      * @param id The id of the message (or null for a random id).
237      */
238     protected void send(Client toClient, String onChannel, Object data, String id)
239     {
240         toClient.deliver(getClient(),onChannel,data,id);
241     }    
242 
243     /* ------------------------------------------------------------ */
244     /** Handle Exception.
245      * This method is called when a mapped subscription method throws
246      * and exception while handling a message.
247      * @param fromClient
248      * @param toClient
249      * @param msg
250      * @param th
251      */
252     protected void exception(Client fromClient, Client toClient, Map<String, Object> msg,Throwable th)
253     {
254         th.printStackTrace();
255     }
256 
257 
258     /* ------------------------------------------------------------ */
259     private void invoke(final Method method,final Client fromClient, final Client toClient, final Message msg)
260     {
261         if (_threadPool==null)
262             doInvoke(method,fromClient,toClient,msg);
263         else
264         {
265             _threadPool.dispatch(new Runnable()
266             {
267                 public void run()
268                 {
269                     try
270                     {
271                         ((MessageImpl)msg).incRef();
272                         doInvoke(method,fromClient,toClient,msg);
273                     }
274                     finally
275                     {
276                         ((MessageImpl)msg).decRef();
277                     }
278                 }   
279             });
280         }
281         
282     }
283     
284     /* ------------------------------------------------------------ */
285     private void doInvoke(Method method,Client fromClient, Client toClient, Message msg)
286     {
287         String channel=(String)msg.get(Bayeux.CHANNEL_FIELD);
288         Object data=msg.get(Bayeux.DATA_FIELD);
289         String id=msg.getId();
290         if (method!=null)
291         {
292             try
293             {
294                 Class<?>[] args = method.getParameterTypes();
295                 Object arg=Message.class.isAssignableFrom(args[1])?msg:data;
296                 
297                 Object reply=null;
298                 switch(method.getParameterTypes().length)
299                 {
300                     case 2:
301                         reply=method.invoke(this,fromClient,arg);
302                         break;
303                     case 3:
304                         reply=method.invoke(this,fromClient,arg,id);
305                         break;
306                     case 4:
307                         reply=method.invoke(this,fromClient,channel,arg,id);
308                         break;
309                 }
310                 
311                 if (reply!=null)
312                     send(fromClient,channel,reply,id);
313             }
314             catch (Exception e)
315             {
316                 exception(fromClient,toClient,msg,e);
317             }
318             catch (Error e)
319             {
320                 exception(fromClient,toClient,msg,e);
321             }
322         }
323     }
324 
325     /* ------------------------------------------------------------ */
326     /* ------------------------------------------------------------ */
327     private class AsyncListen implements MessageListener, MessageListener.Asynchronous
328     {
329         public void deliver(Client fromClient, Client toClient, Message msg)
330         {
331             String channel=(String)msg.get(Bayeux.CHANNEL_FIELD);
332             Method method=_methods.get(channel);
333             invoke(method,fromClient,toClient,msg);
334         }
335     }
336     
337     /* ------------------------------------------------------------ */
338     /* ------------------------------------------------------------ */
339     private class SyncListen implements MessageListener, MessageListener.Synchronous
340     {
341         public void deliver(Client fromClient, Client toClient, Message msg)
342         {
343             String channel=(String)msg.get(Bayeux.CHANNEL_FIELD);
344             Method method=_methods.get(channel);
345             invoke(method,fromClient,toClient,msg);
346         }
347     }
348     
349 
350     /* ------------------------------------------------------------ */
351     /* ------------------------------------------------------------ */
352     private class SyncWildListen implements MessageListener, MessageListener.Synchronous
353     {
354         Method _method;
355         public SyncWildListen(Method method)
356         {
357             _method=method;
358         }
359         public void deliver(Client fromClient, Client toClient, Message msg)
360         {
361             invoke(_method,fromClient,toClient,msg);
362         }
363     };
364     
365 
366     /* ------------------------------------------------------------ */
367     /* ------------------------------------------------------------ */
368     private class AsyncWildListen implements MessageListener, MessageListener.Asynchronous
369     {
370         Method _method;
371         public AsyncWildListen(Method method)
372         {
373             _method=method;
374         }
375         public void deliver(Client fromClient, Client toClient, Message msg)
376         {
377             invoke(_method,fromClient,toClient,msg);
378         }
379     };
380 
381 }
382