View Javadoc

1   package org.cometd.oort;
2   
3   import java.io.IOException;
4   import java.security.SecureRandom;
5   import java.util.ArrayList;
6   import java.util.HashMap;
7   import java.util.HashSet;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Random;
11  import java.util.Set;
12  import java.util.Timer;
13  import java.util.concurrent.ConcurrentHashMap;
14  import java.util.concurrent.ConcurrentMap;
15  
16  import org.cometd.Bayeux;
17  import org.cometd.Channel;
18  import org.cometd.Client;
19  import org.cometd.ClientBayeuxListener;
20  import org.cometd.Extension;
21  import org.cometd.Message;
22  import org.cometd.MessageListener;
23  import org.cometd.QueueListener;
24  import org.cometd.RemoveListener;
25  import org.mortbay.component.AbstractLifeCycle;
26  import org.mortbay.jetty.HttpURI;
27  import org.mortbay.jetty.client.HttpClient;
28  import org.mortbay.log.Log;
29  import org.mortbay.util.ajax.JSON;
30  
31  /* ------------------------------------------------------------ */
32  /**
33   * Oort cluster of cometd servers.
34   * <p>
35   * This class maintains a collection of {@link OortComet} instances to each
36   * comet server identified by calls to {@link #observeComet(String)}. The Oort
37   * instance is created and configured by {@link OortServlet}. 
38   * <p>
39   * The key configuration parameter that must be set is the Oort URL, which is 
40   * full public URL to the cometd servlet, eg. http://myserver:8080/context/cometd
41   * See {@link OortServlet} for more configuration detail.<p>
42   * @author gregw
43   *
44   */
45  public class Oort extends AbstractLifeCycle
46  {
47      public final static String OORT_URL = "oort.url";
48      public final static String OORT_CLOUD = "oort.cloud";
49      public final static String OORT_CHANNELS = "oort.channels";
50      public final static String OORT_ATTRIBUTE = "org.cometd.oort.Oort";
51      
52      protected String _url;
53      protected String _secret;
54      protected Bayeux _bayeux;
55      protected HttpClient _httpClient=new HttpClient();
56      protected Timer _timer=new Timer();
57      protected Random _random=new SecureRandom();
58      protected Client _oortClient;
59      protected List<MessageListener> _oortMessageListeners = new ArrayList<MessageListener>();
60      
61      protected Map<String,OortComet> _knownCommets = new HashMap<String,OortComet>();
62      protected Set<String> _channels = new HashSet<String>();
63  
64      /* ------------------------------------------------------------ */
65      Oort(String id,Bayeux bayeux)
66      {
67          _url=id;
68          _bayeux=bayeux;
69          _secret=Long.toHexString(_random.nextLong());
70          
71          _oortClient=_bayeux.newClient("oort");
72          _oortClient.addListener(new RootOortClientListener());
73          _bayeux.getChannel("/oort/cloud",true).subscribe(_oortClient);
74          bayeux.addExtension(new OortExtension());
75      }
76  
77      /* ------------------------------------------------------------ */
78      public Bayeux getBayeux()
79      {
80          return _bayeux;
81      }
82      
83      /* ------------------------------------------------------------ */
84      /**
85       * @return The oublic absolute URL of the Oort cometd server.
86       */
87      public String getURL()
88      {
89          return _url;
90      }
91  
92      /* ------------------------------------------------------------ */
93      public String getSecret()
94      {
95          return _secret;
96      }
97  
98      /* ------------------------------------------------------------ */
99      protected void doStart() throws Exception
100     {
101         super.doStart();
102         _httpClient.start();
103     }
104 
105     /* ------------------------------------------------------------ */
106     /**
107      * Observe an Oort Comet server.
108      * <p>
109      * The the comet server is not already observed, start a {@link OortComet} 
110      * instance for it.
111      * 
112      * @param cometUrl
113      * @return The {@link OortComet} instance for the comet server.
114      */
115     public OortComet observeComet(String cometUrl)
116     {
117         synchronized (this)
118         {
119             if (_url.equals(cometUrl))
120                 return null;
121             OortComet comet = _knownCommets.get(cometUrl);
122             if (comet==null)
123             {
124                 try
125                 {
126                     comet = new OortComet(this,cometUrl);
127                     _knownCommets.put(cometUrl,comet);
128                     comet.start();
129                 }
130                 catch(Exception e)
131                 {
132                     throw new IllegalStateException(e);
133                 }
134             }
135             return comet;
136         }
137     }
138 
139     /* ------------------------------------------------------------ */
140     /**
141      * Pass observed comets. 
142      * <p>
143      * Called when another comet server publishes it's list of 
144      * known comets to the /oort/cloud channel.  If the list contains
145      * any unknown commets, then {@link #observeComet(String)} is 
146      * called for each.
147      * @param comets
148      */
149     void observedComets(Set<String> comets)
150     {
151         synchronized (this)
152         {
153             Set<String> known=getKnownComets();
154             for (String comet : comets)
155                 if (!_url.equals(comet))
156                     observeComet(comet);
157             known=getKnownComets();
158             
159             if (!comets.containsAll(known))
160                 _bayeux.getChannel("/oort/cloud",true).publish(_oortClient,known,null);
161         }
162     }
163 
164     /* ------------------------------------------------------------ */
165     /**
166      * @return The set of known Oort comet servers URLs.
167      */
168     public Set<String> getKnownComets()
169     {
170         synchronized (this)
171         {
172             Set<String> comets = new HashSet<String>(_knownCommets.keySet());
173             comets.add(_url);
174             return comets;
175         }
176     }
177 
178     /* ------------------------------------------------------------ */
179     /**
180      * Observer a channel.
181      * <p>
182      * Once observed, all {@link OortComet} instances subscribe
183      * to the channel and will repeat any messages published to
184      * the local channel (with loop prevention), so that the
185      * messages are distributed to all Oort comet servers.
186      * @param channelId
187      */
188     public void observeChannel(String channelId)
189     {
190         synchronized (this)
191         {
192             if (!_channels.contains(channelId))
193             {
194                 _channels.add(channelId);
195                 for (OortComet comet : _knownCommets.values())
196                     if (comet.isHandshook())
197                         comet.subscribe(channelId);
198             }
199         }
200     }
201 
202     /* ------------------------------------------------------------ */
203     /**
204      * Add a MessageListener that will receive all messages 
205      * published on /oort/* channels on connected OortComets 
206      * @param listener
207      */
208     public void addOortMessageListener(MessageListener listener)
209     {
210         synchronized (this)
211         {
212             _oortMessageListeners.add(listener);
213         }
214     }
215 
216     /* ------------------------------------------------------------ */
217     /**
218      * Remove an Oort message listener.
219      * @param listener
220      * @return true if the listener was removed.
221      */
222     public boolean removeOortClientListener(MessageListener listener)
223     {
224         synchronized (this)
225         {
226             return _oortMessageListeners.remove(listener);
227         }
228     }
229 
230     /* ------------------------------------------------------------ */
231     public boolean isOort(Client client)
232     {
233         return client==_oortClient;
234     }
235 
236     /* ------------------------------------------------------------ */
237     public String toString()
238     {
239         return _url;
240     }
241 
242     /* ------------------------------------------------------------ */
243     /**
244      * Called to register the details of a successful handshake with an
245      * Oort comet.  A {@link RemoteOortClientListener} instance is added to
246      * the local Oort client instance.
247      * @param oortUrl
248      * @param oortSecret
249      * @param clientId
250      */
251     protected void oortHandshook(String oortUrl,String oortSecret,String clientId)
252     {
253         Log.info(this+": "+clientId+" is oort "+oortUrl);
254         if (!_knownCommets.containsKey(oortUrl))
255             observeComet(oortUrl);
256         
257         Client client = _bayeux.getClient(clientId);
258         
259         client.addExtension(new RemoteOortClientExtension());
260     }
261 
262     /* ------------------------------------------------------------ */
263     /* ------------------------------------------------------------ */
264     /**
265      * Extension to detect incoming handshake from other Oort servers
266      * and to call {@link Oort#oortHandshook(String, String, String)}.
267      *
268      */
269     protected class OortExtension implements Extension
270     {
271         public Message rcv(Client from, Message message)
272         {
273             return message;
274         }
275 
276         public Message rcvMeta(Client from, Message message)
277         {
278             return message;
279         }
280 
281         public Message send(Client from, Message message)
282         {
283             return message;
284         }
285 
286         public Message sendMeta(Client from, Message message)
287         {
288             if (message.getChannel().equals(Bayeux.META_HANDSHAKE) && Boolean.TRUE.equals(message.get(Bayeux.SUCCESSFUL_FIELD)))
289             {
290                 Message rcv = message.getAssociated();
291                 System.err.println(_url+" --> "+rcv);
292                 
293                 Map<String,Object> rcvExt = (Map<String,Object>)rcv.get("ext");
294                 if (rcvExt!=null)
295                 {
296                     Map<String,Object> oort = (Map<String,Object>)rcvExt.get("oort");
297                     if (oort!=null)
298                     {
299                         String cometUrl = (String)oort.get("comet");
300                         String oortUrl = (String)oort.get("oort");
301 
302                         if (getURL().equals(cometUrl))
303                         {
304                             String oortSecret = (String)oort.get("oortSecret");
305                             
306                             oortHandshook(oortUrl,oortSecret,message.getClientId());
307                             
308                             Object ext=message.get("ext");
309                             
310                             Map<String,Object> sndExt = (Map<String,Object>)((ext instanceof JSON.Literal)?JSON.parse(ext.toString()):ext);
311                             if (sndExt==null)
312                                 sndExt = new HashMap<String,Object>();
313                             oort.put("cometSecret",getSecret());
314                             sndExt.put("oort",oort);
315                             message.put("ext",sndExt);
316                         }
317                     }
318                 }
319                 System.err.println(_url+" <-- "+message);
320             }
321             return message;
322         }   
323     }
324 
325     /* ------------------------------------------------------------ */
326     /* ------------------------------------------------------------ */
327     /**
328      * An Extension installed on clients for remote Oort servers
329      * that prevents publish loops.
330      */
331     protected class RemoteOortClientExtension implements Extension
332     {
333         public boolean queueMaxed(Client from, Client client, Message message)
334         {
335             // avoid loops
336             boolean send = from!=_oortClient || message.getChannel().startsWith("/oort/");
337             return send;
338         }
339 
340         public Message rcv(Client from, Message message)
341         {
342             return message;
343         }
344 
345         public Message rcvMeta(Client from, Message message)
346         {
347             return message;
348         }
349 
350         public Message send(Client from, Message message)
351         {
352             // avoid loops
353             boolean send = !isOort(from) || message.getChannel().startsWith("/oort/");
354             return send?message:null;
355         }
356 
357         public Message sendMeta(Client from, Message message)
358         {
359             return message;
360         }
361     }
362 
363     /* ------------------------------------------------------------ */
364     /* ------------------------------------------------------------ */
365     /**
366      * MessageListener that handles publishes to /oort/cloud
367      */
368     protected class RootOortClientListener implements RemoveListener, MessageListener
369     {
370         public void removed(String clientId, boolean timeout)
371         {
372             // TODO
373         }
374 
375         public void deliver(Client fromClient, Client toClient, Message msg)
376         {
377             String channelId = msg.getChannel();
378             if (msg.getData()!=null)
379             {
380                 if (channelId.equals("/oort/cloud") && msg.getData() instanceof Object[])
381                 {
382                     Object[] data = (Object[])msg.getData();
383                     Set<String> comets = new HashSet<String>();
384                     for (Object o:data)
385                         comets.add(o.toString());
386                     observedComets(comets);
387                 }   
388             }
389         }
390         
391     }
392 }