View Javadoc

1   // ========================================================================
2   // Copyright 2007 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  
15  package org.mortbay.cometd;
16  
17  import java.io.FileNotFoundException;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.InputStreamReader;
21  import java.util.ArrayList;
22  import java.util.List;
23  import java.util.Map;
24  
25  import javax.servlet.GenericServlet;
26  import javax.servlet.ServletException;
27  import javax.servlet.ServletRequest;
28  import javax.servlet.ServletResponse;
29  import javax.servlet.http.Cookie;
30  import javax.servlet.http.HttpServlet;
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpServletResponse;
33  
34  import org.cometd.Bayeux;
35  import org.cometd.DataFilter;
36  import org.cometd.Message;
37  import org.mortbay.cometd.filter.JSONDataFilter;
38  import org.mortbay.log.Log;
39  import org.mortbay.util.ajax.JSON;
40  
41  
42  /**
43   * Cometd Filter Servlet implementing the {@link AbstractBayeux} protocol.
44   * 
45   * The Servlet can be initialized with a json file mapping channels to
46   * {@link DataFilter} definitions. The servlet init parameter "filters" should
47   * point to a webapplication resource containing a JSON array of filter
48   * definitions. For example:
49   * 
50   * <pre>
51   *  [
52   *    { 
53   *      &quot;channels&quot;: &quot;/**&quot;,
54   *      &quot;class&quot;   : &quot;org.mortbay.cometd.filter.NoMarkupFilter&quot;,
55   *      &quot;init&quot;    : {}
56   *    }
57   *  ]
58   * </pre>
59   * The following init parameters can be used to configure the servlet:<dl>
60   * <dt>timeout</dt>
61   * <dd>The server side poll timeout in milliseconds (default 250000). This is how
62   * long the server will hold a reconnect request before responding.</dd>
63   * 
64   * <dt>interval</dt>
65   * <dd>The client side poll timeout in milliseconds (default 0). How long a client
66   * will wait between reconnects</dd>
67   * 
68   * <dt>maxInterval</dt>
69   * <dd>The max client side poll timeout in milliseconds (default 30000). A client will
70   * be removed if a connection is not received in this time.
71   * 
72   * <dt>multiFrameInterval</dt>
73   * <dd>the client side poll timeout
74   * if multiple connections are detected from the same browser (default 1500).</dd>
75   * 
76   * <dt>JSONCommented</dt>
77   * <dd>If "true" then the server will accept JSON wrapped
78   * in a comment and will generate JSON wrapped in a comment. This is a defence against
79   * Ajax Hijacking.</dd>
80   * 
81   * <dt>filters</dt>
82   * <dd>the location of a JSON file describing {@link DataFilter} instances to be installed</dd>
83   * 
84   * <dt>requestAvailable</dt>
85   * <dd>If true, the current request is made available via the {@link AbstractBayeux#getCurrentRequest()} method</dd>
86   * 
87   * <dt>loglevel</dt>
88   * <dd>0=none, 1=info, 2=debug</dd>
89   * 
90   * <dt>directDeliver</dt>
91   * <dd>true if published messages are delivered directly to subscribers (default). If false, a message copy is created with only supported fields (default true).</dd>
92   * 
93   * <dt>refsThreshold</dt>
94   * <dd>The number of message refs at which the a single message response will be 
95   * cached instead of being generated for every client delivered to. Done to optimize 
96   * a single message being sent to multiple clients.</dd>
97   * </dl>
98   * 
99   * @author gregw
100  * @author aabeling: added JSONP transport
101  * 
102  * @see {@link AbstractBayeux}
103  * @see {@link ChannelId}
104  */
105 public abstract class AbstractCometdServlet extends GenericServlet
106 {
107     public static final String CLIENT_ATTR="org.mortbay.cometd.client";
108     public static final String TRANSPORT_ATTR="org.mortbay.cometd.transport";
109     public static final String MESSAGE_PARAM="message";
110     public static final String TUNNEL_INIT_PARAM="tunnelInit";
111     public static final String HTTP_CLIENT_ID="BAYEUX_HTTP_CLIENT";
112     public final static String BROWSER_ID="BAYEUX_BROWSER";
113     
114     protected AbstractBayeux _bayeux;
115     public final static int __DEFAULT_REFS_THRESHOLD = 1;
116     protected int _refsThreshold=__DEFAULT_REFS_THRESHOLD;
117 
118     public AbstractBayeux getBayeux()
119     {
120         return _bayeux;
121     }
122     
123     protected abstract AbstractBayeux newBayeux();
124 
125     @Override
126     public void init() throws ServletException
127     {
128         synchronized (AbstractCometdServlet.class)
129         {
130             _bayeux=(AbstractBayeux)getServletContext().getAttribute(Bayeux.DOJOX_COMETD_BAYEUX);
131             if (_bayeux==null)
132             {    
133                 _bayeux=newBayeux(); 
134             }
135         }
136         
137         synchronized(_bayeux)
138         {
139             boolean was_initialized=_bayeux.isInitialized();
140             _bayeux.initialize(getServletContext());
141             
142             if (!was_initialized)
143             {
144                 String filters=getInitParameter("filters");
145                 if (filters!=null)
146                 {
147                     try
148                     {
149                         InputStream is = getServletContext().getResourceAsStream(filters);
150                         if (is==null)
151                             throw new FileNotFoundException(filters);
152                         
153                         Object[] objects=(Object[])JSON.parse(new InputStreamReader(getServletContext().getResourceAsStream(filters),"utf-8"));
154                         for (int i=0; objects!=null&&i<objects.length; i++)
155                         {
156                             Map<?,?> filter_def=(Map<?,?>)objects[i];
157 
158                             String fc = (String)filter_def.get("class");
159                             if (fc!=null)
160                                 Log.warn(filters+" file uses deprecated \"class\" name. Use \"filter\" instead");
161                             else
162                                 fc=(String)filter_def.get("filter");
163                             Class<?> c=Thread.currentThread().getContextClassLoader().loadClass(fc);
164                             DataFilter filter=(DataFilter)c.newInstance();
165 
166                             if (filter instanceof JSONDataFilter)
167                                 ((JSONDataFilter)filter).init(filter_def.get("init"));
168 
169                             _bayeux.getChannel((String)filter_def.get("channels"),true).addDataFilter(filter);
170                         }
171                     }
172                     catch (Exception e)
173                     {
174                         getServletContext().log("Could not parse: "+filters,e);
175                         throw new ServletException(e);
176                     }
177                 }
178 
179                 String timeout=getInitParameter("timeout");
180                 if (timeout!=null)
181                     _bayeux.setTimeout(Long.parseLong(timeout));
182                 
183                 String maxInterval=getInitParameter("maxInterval");
184                 if (maxInterval!=null)
185                     _bayeux.setMaxInterval(Long.parseLong(maxInterval));
186 
187                 String commentedJSON=getInitParameter("JSONCommented");
188                 _bayeux.setJSONCommented(commentedJSON!=null && Boolean.parseBoolean(commentedJSON));
189 
190                 String l=getInitParameter("logLevel");
191                 if (l!=null&&l.length()>0)
192                     _bayeux.setLogLevel(Integer.parseInt(l));
193                 
194                 String interval=getInitParameter("interval");
195                 if (interval!=null)
196                     _bayeux.setInterval(Long.parseLong(interval));
197                 
198                 String mfInterval=getInitParameter("multiFrameInterval");
199                 if (mfInterval!=null)
200                     _bayeux.setMultiFrameInterval(Integer.parseInt(mfInterval));
201 
202                 String requestAvailable=getInitParameter("requestAvailable");
203                 _bayeux.setRequestAvailable(requestAvailable!=null && Boolean.parseBoolean(requestAvailable));
204 
205                 String direct=getInitParameter("directDeliver");
206                 if (direct!=null)
207                     _bayeux.setDirectDeliver(Boolean.parseBoolean(direct));
208 
209                 String async=getInitParameter("asyncDeliver");
210                 if (async!=null)
211                     getServletContext().log("asyncDeliver no longer supported");
212              
213                 String refsThreshold=getInitParameter("refsThreshold");
214                 if (refsThreshold!=null)
215                     _refsThreshold=Integer.parseInt(refsThreshold);
216 
217                 _bayeux.generateAdvice();
218                 
219                 if (_bayeux.isLogInfo())
220                 {
221                     getServletContext().log("timeout="+timeout);
222                     getServletContext().log("interval="+interval);
223                     getServletContext().log("maxInterval="+maxInterval);
224                     getServletContext().log("multiFrameInterval="+mfInterval);
225                     getServletContext().log("filters="+filters);
226                     getServletContext().log("refsThreshold="+refsThreshold);
227                 }
228             }
229         }
230 
231         getServletContext().setAttribute(Bayeux.DOJOX_COMETD_BAYEUX,_bayeux);
232     }
233 
234     protected abstract void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException;
235     
236     @Override
237     public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException
238     {
239         HttpServletRequest request=(HttpServletRequest)req;
240         HttpServletResponse response=(HttpServletResponse)resp;
241         
242         if (_bayeux.isRequestAvailable())
243             _bayeux.setCurrentRequest(request);
244         try
245         {
246             service(request,response);
247         }
248         finally
249         {
250             if (_bayeux.isRequestAvailable())
251                 _bayeux.setCurrentRequest(null);
252         }
253     }
254 
255 
256     protected String browserId(HttpServletRequest request)
257     {
258         Cookie[] cookies = request.getCookies();
259         if (cookies!=null)
260         {
261             for (Cookie cookie : cookies)
262             {
263                 if (BROWSER_ID.equals(cookie.getName()))
264                     return cookie.getValue();
265             }
266         }
267         
268         return null;
269     }
270 
271     protected String newBrowserId(HttpServletRequest request,HttpServletResponse response)
272     {
273         String browser_id=Long.toHexString(request.getRemotePort())+
274         Long.toString(_bayeux.getRandom(),36)+
275         Long.toString(System.currentTimeMillis(),36)+
276         Long.toString(request.getRemotePort(),36);
277         
278         Cookie cookie = new Cookie(BROWSER_ID,browser_id);
279         cookie.setPath("/");
280         cookie.setMaxAge(-1);
281         response.addCookie(cookie);
282         return browser_id;
283     }
284     
285     private static Message[] __EMPTY_BATCH=new Message[0];
286 
287     protected Message[] getMessages(HttpServletRequest request) throws IOException
288     {
289         String fodder=null;
290         try
291         {
292             // Get message batches either as JSON body or as message parameters
293             if (request.getContentType() != null && !request.getContentType().startsWith("application/x-www-form-urlencoded"))
294             {
295                 return _bayeux.parse(request.getReader());
296             }
297 
298             String[] batches=request.getParameterValues(MESSAGE_PARAM);
299 
300             if (batches==null || batches.length==0)
301                 return __EMPTY_BATCH;
302 
303             if (batches.length==0)
304             {
305                 fodder=batches[0];
306                 return _bayeux.parse(fodder);
307             }
308 
309             List<Message> messages = new ArrayList<Message>();
310             for (int i=0;i<batches.length;i++)
311             {
312                 if (batches[i]==null)
313                     continue;
314 
315                 fodder=batches[i];
316                 _bayeux.parseTo(fodder,messages);
317                 
318             }
319 
320             return messages.toArray(new Message[messages.size()]);
321         }
322         catch(IOException e)
323         {
324             throw e;
325         }
326         catch(Exception e)
327         {
328             throw new Error(fodder,e);
329         }
330     }
331 
332 }