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