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