// ========================================================================
// Copyright 2007 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//========================================================================
package org.mortbay.cometd;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.cometd.Bayeux;
import org.cometd.DataFilter;
import org.cometd.Message;
import org.mortbay.cometd.filter.JSONDataFilter;
import org.mortbay.log.Log;
import org.mortbay.util.ajax.JSON;
/**
* Cometd Filter Servlet implementing the {@link AbstractBayeux} protocol.
*
* The Servlet can be initialized with a json file mapping channels to
* {@link DataFilter} definitions. The servlet init parameter "filters" should
* point to a webapplication resource containing a JSON array of filter
* definitions. For example:
*
*
* [
* {
* "channels": "/**",
* "class" : "org.mortbay.cometd.filter.NoMarkupFilter",
* "init" : {}
* }
* ]
*
*
* The following init parameters can be used to configure the servlet:
*
* - timeout
* - The server side poll timeout in milliseconds (default 250000). This is
* how long the server will hold a reconnect request before responding.
*
* - interval
* - The client side poll timeout in milliseconds (default 0). How long a
* client will wait between reconnects
*
* - maxInterval
* - The max client side poll timeout in milliseconds (default 30000). A
* client will be removed if a connection is not received in this time.
*
*
- maxLazyLatency
* - The max time in ms(default 0) that a client with lazy messages will wait before
* sending a response. If 0, then the client will wait until the next timeout or
* non-lazy message.
*
*
- multiFrameInterval
* - the client side poll timeout if multiple connections are detected from
* the same browser (default 1500).
*
* - JSONCommented
* - If "true" then the server will accept JSON wrapped in a comment and will
* generate JSON wrapped in a comment. This is a defence against Ajax Hijacking.
*
*
* - filters
* - the location of a JSON file describing {@link DataFilter} instances to be
* installed
*
* - requestAvailable
* - If true, the current request is made available via the
* {@link AbstractBayeux#getCurrentRequest()} method
*
* - loglevel
* - 0=none, 1=info, 2=debug
*
* - refsThreshold
* - The number of message refs at which the a single message response will be
* cached instead of being generated for every client delivered to. Done to
* optimize a single message being sent to multiple clients.
*
*
* @author gregw
* @author aabeling: added JSONP transport
*
* @see {@link AbstractBayeux}
* @see {@link ChannelId}
*/
public abstract class AbstractCometdServlet extends GenericServlet
{
public static final String CLIENT_ATTR="org.mortbay.cometd.client";
public static final String TRANSPORT_ATTR="org.mortbay.cometd.transport";
public static final String MESSAGE_PARAM="message";
public static final String TUNNEL_INIT_PARAM="tunnelInit";
public static final String HTTP_CLIENT_ID="BAYEUX_HTTP_CLIENT";
public final static String BROWSER_ID="BAYEUX_BROWSER";
protected AbstractBayeux _bayeux;
public final static int __DEFAULT_REFS_THRESHOLD=0;
protected int _refsThreshold=__DEFAULT_REFS_THRESHOLD;
public AbstractBayeux getBayeux()
{
return _bayeux;
}
protected abstract AbstractBayeux newBayeux();
@Override
public void init() throws ServletException
{
synchronized(AbstractCometdServlet.class)
{
_bayeux=(AbstractBayeux)getServletContext().getAttribute(Bayeux.ATTRIBUTE);
if (_bayeux == null)
{
_bayeux=newBayeux();
}
}
synchronized(_bayeux)
{
boolean was_initialized=_bayeux.isInitialized();
_bayeux.initialize(getServletContext());
if (!was_initialized)
{
String filters=getInitParameter("filters");
if (filters != null)
{
try
{
InputStream is=getServletContext().getResourceAsStream(filters);
if (is == null)
throw new FileNotFoundException(filters);
Object[] objects=(Object[])JSON.parse(new InputStreamReader(getServletContext().getResourceAsStream(filters),"utf-8"));
for (int i=0; objects != null && i < objects.length; i++)
{
Map,?> filter_def=(Map,?>)objects[i];
String fc=(String)filter_def.get("class");
if (fc != null)
Log.warn(filters + " file uses deprecated \"class\" name. Use \"filter\" instead");
else
fc=(String)filter_def.get("filter");
Class> c=Thread.currentThread().getContextClassLoader().loadClass(fc);
DataFilter filter=(DataFilter)c.newInstance();
if (filter instanceof JSONDataFilter)
((JSONDataFilter)filter).init(filter_def.get("init"));
_bayeux.getChannel((String)filter_def.get("channels"),true).addDataFilter(filter);
}
}
catch(Exception e)
{
getServletContext().log("Could not parse: " + filters,e);
throw new ServletException(e);
}
}
String timeout=getInitParameter("timeout");
if (timeout != null)
_bayeux.setTimeout(Long.parseLong(timeout));
String maxInterval=getInitParameter("maxInterval");
if (maxInterval != null)
_bayeux.setMaxInterval(Long.parseLong(maxInterval));
String commentedJSON=getInitParameter("JSONCommented");
_bayeux.setJSONCommented(commentedJSON != null && Boolean.parseBoolean(commentedJSON));
String l=getInitParameter("logLevel");
if (l != null && l.length() > 0)
_bayeux.setLogLevel(Integer.parseInt(l));
String interval=getInitParameter("interval");
if (interval != null)
_bayeux.setInterval(Long.parseLong(interval));
String maxLazy=getInitParameter("maxLazyLatency");
if (maxLazy != null)
_bayeux.setMaxLazyLatency(Integer.parseInt(maxLazy));
String mfInterval=getInitParameter("multiFrameInterval");
if (mfInterval != null)
_bayeux.setMultiFrameInterval(Integer.parseInt(mfInterval));
String requestAvailable=getInitParameter("requestAvailable");
_bayeux.setRequestAvailable(requestAvailable != null && Boolean.parseBoolean(requestAvailable));
String async=getInitParameter("asyncDeliver");
if (async != null)
getServletContext().log("asyncDeliver no longer supported");
String refsThreshold=getInitParameter("refsThreshold");
if (refsThreshold != null)
_refsThreshold=Integer.parseInt(refsThreshold);
_bayeux.generateAdvice();
if (_bayeux.isLogInfo())
{
getServletContext().log("timeout=" + timeout);
getServletContext().log("interval=" + interval);
getServletContext().log("maxInterval=" + maxInterval);
getServletContext().log("multiFrameInterval=" + mfInterval);
getServletContext().log("filters=" + filters);
getServletContext().log("refsThreshold=" + refsThreshold);
}
}
}
getServletContext().setAttribute(Bayeux.ATTRIBUTE,_bayeux);
}
protected abstract void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException;
@Override
public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException
{
HttpServletRequest request=(HttpServletRequest)req;
HttpServletResponse response=(HttpServletResponse)resp;
if (_bayeux.isRequestAvailable())
_bayeux.setCurrentRequest(request);
try
{
service(request,response);
}
finally
{
if (_bayeux.isRequestAvailable())
_bayeux.setCurrentRequest(null);
}
}
protected String findBrowserId(HttpServletRequest request)
{
Cookie[] cookies=request.getCookies();
if (cookies != null)
{
for (Cookie cookie : cookies)
{
if (BROWSER_ID.equals(cookie.getName()))
return cookie.getValue();
}
}
return null;
}
protected String setBrowserId(HttpServletRequest request, HttpServletResponse response)
{
String browser_id=Long.toHexString(request.getRemotePort()) + Long.toString(_bayeux.getRandom(),36) + Long.toString(System.currentTimeMillis(),36)
+ Long.toString(request.getRemotePort(),36);
Cookie cookie=new Cookie(BROWSER_ID,browser_id);
cookie.setPath("/");
cookie.setMaxAge(-1);
response.addCookie(cookie);
return browser_id;
}
private static Message[] __EMPTY_BATCH=new Message[0];
protected Message[] getMessages(HttpServletRequest request) throws IOException
{
String fodder=null;
try
{
// Get message batches either as JSON body or as message parameters
if (request.getContentType() != null && !request.getContentType().startsWith("application/x-www-form-urlencoded"))
{
return _bayeux.parse(request.getReader());
}
String[] batches=request.getParameterValues(MESSAGE_PARAM);
if (batches == null || batches.length == 0)
return __EMPTY_BATCH;
if (batches.length == 0)
{
fodder=batches[0];
return _bayeux.parse(fodder);
}
List messages=new ArrayList();
for (int i=0; i < batches.length; i++)
{
if (batches[i] == null)
continue;
fodder=batches[i];
_bayeux.parseTo(fodder,messages);
}
return messages.toArray(new Message[messages.size()]);
}
catch(IOException e)
{
throw e;
}
catch(Exception e)
{
throw new Error(fodder,e);
}
}
}