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