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