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 }