/*
* Copyright 2009 Richard Zschech.
*
* 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 net.zschech.gwt.comet.server;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import net.zschech.gwt.comet.client.impl.CometTransport;
import net.zschech.gwt.comet.server.impl.AsyncServlet;
import net.zschech.gwt.comet.server.impl.CometServletResponseImpl;
import net.zschech.gwt.comet.server.impl.CometSessionImpl;
import net.zschech.gwt.comet.server.impl.EventSourceCometServletResponse;
import net.zschech.gwt.comet.server.impl.HTTPRequestCometServletResponse;
import net.zschech.gwt.comet.server.impl.IEHTMLFileCometServletResponse;
import net.zschech.gwt.comet.server.impl.OperaEventSourceCometServletResponse;
import com.google.gwt.rpc.server.ClientOracle;
import com.google.gwt.rpc.server.HostedModeClientOracle;
import com.google.gwt.rpc.server.WebModeClientOracle;
import com.google.gwt.user.server.rpc.SerializationPolicy;
/**
* This is the base class for application's Comet servlets. To process a Comet request override
* {@link #doComet(CometServletResponse)} and send messages by calling {@link CometServletResponse#write(Serializable)}
* or enqueue messages using {@link CometServletResponse#getSession()} and {@link CometSession#enqueue(Serializable)}.
*
* @author Richard Zschech
*/
public class CometServlet extends HttpServlet {
public static final String AUTO_CREATE_COMET_SESSION = "net.zschech.gwt.comet.server.auto.create.comet.session.on.comet.request";
private static final long serialVersionUID = 820972291784919880L;
private int heartbeat = 15 * 1000; // 15 seconds by default
private transient AsyncServlet async;
private boolean autoCreateCometSession;
public void setHeartbeat(int heartbeat) {
this.heartbeat = heartbeat;
}
public int getHeartbeat() {
return heartbeat;
}
@Override
public void init() throws ServletException {
ServletConfig servletConfig = getServletConfig();
String heartbeat = servletConfig.getInitParameter("heartbeat");
if (heartbeat != null) {
this.heartbeat = Integer.parseInt(heartbeat);
}
this.autoCreateCometSession = "true".equals(getServletConfig().getInitParameter(AUTO_CREATE_COMET_SESSION));
async = AsyncServlet.initialize(getServletContext());
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
int requestHeartbeat = getHeartbeat();
String requestedHeartbeat = request.getParameter("heartbeat");
if (requestedHeartbeat != null) {
try {
requestHeartbeat = Integer.parseInt(requestedHeartbeat);
if (requestHeartbeat <= 0) {
throw new IOException("invalid heartbeat parameter");
}
}
catch (NumberFormatException e) {
throw new IOException("invalid heartbeat parameter");
}
}
ClientOracle clientOracle = getClientOracle(request);
SerializationPolicy serializationPolicy = clientOracle == null ? createSerializationPolicy() : null;
CometServletResponseImpl cometServletResponse = createCometServletResponse(request, response, serializationPolicy, clientOracle, requestHeartbeat);
doCometImpl(cometServletResponse);
}
catch (IOException e) {
CometServletResponseImpl cometServletResponse = createCometServletResponse(request, response, null, null, 0);
cometServletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
}
}
private CometServletResponseImpl createCometServletResponse(HttpServletRequest request, HttpServletResponse response, SerializationPolicy serializationPolicy, ClientOracle clientOracle, int requestHeartbeat) {
String accept = request.getHeader("Accept");
String userAgent = request.getHeader("User-Agent");
if ("text/event-stream".equals(accept)) {
return new EventSourceCometServletResponse(request, response, serializationPolicy, clientOracle, this, async, requestHeartbeat);
}
else if ("application/comet".equals(accept)) {
return new HTTPRequestCometServletResponse(request, response, serializationPolicy, clientOracle, this, async, requestHeartbeat);
}
else if (userAgent != null && userAgent.contains("Opera")) {
return new OperaEventSourceCometServletResponse(request, response, serializationPolicy, clientOracle, this, async, requestHeartbeat);
}
else {
return new IEHTMLFileCometServletResponse(request, response, serializationPolicy, clientOracle, this, async, requestHeartbeat);
}
}
private void doCometImpl(CometServletResponseImpl response) throws IOException {
try {
// setup the request
response.initiate();
if (autoCreateCometSession) {
response.getSession();
}
// call the application code
doComet(response);
}
catch (IOException e) {
log("Error calling doComet()", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
catch (ServletException e) {
log("Error calling doComet()", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
// at this point the application may have spawned threads to process this response
// so we have to be careful about concurrency from here on
response.suspend();
}
/**
* Override this method to process a new comet request. All required information from the {@link HttpServletRequest}
* must be retrieved {@link CometServletResponse#getRequest()} in this method as it will not be available after this
* method returns and the request is suspended. This method may write data to the Comet response but should not
* block. Writing data from this method before the request is suspended can improve the efficiency because padding
* data may not be needed to cause the browser to start processing the stream.
*
* @param cometResponse
* @throws ServletException
* @throws IOException
*/
protected void doComet(CometServletResponse cometResponse) throws ServletException, IOException {
}
/**
* Override this method to be notified of the Comet connection being terminated.
*
* @param cometResponse
* @param serverInitiated
*/
public void cometTerminated(CometServletResponse cometResponse, boolean serverInitiated) {
}
/**
* Override this method to override the requested heartbeat. By default only requested heartbeats > this.heartbeat
* are allowed.
*
* @param requestedHeartbeat
* @return
*/
protected int getHeartbeat(int requestedHeartbeat) {
return requestedHeartbeat < heartbeat ? heartbeat : requestedHeartbeat;
}
protected SerializationPolicy createSerializationPolicy() {
return new SerializationPolicy() {
@Override
public boolean shouldDeserializeFields(final Class<?> clazz) {
throw new UnsupportedOperationException("shouldDeserializeFields");
}
@Override
public boolean shouldSerializeFields(final Class<?> clazz) {
return Object.class != clazz;
}
@Override
public void validateDeserialize(final Class<?> clazz) {
throw new UnsupportedOperationException("validateDeserialize");
}
@Override
public void validateSerialize(final Class<?> clazz) {
}
};
}
private final Map<String, SoftReference<ClientOracle>> clientOracleCache = new HashMap<String, SoftReference<ClientOracle>>();
protected ClientOracle getClientOracle(HttpServletRequest request) throws IOException {
String permutationStrongName = request.getParameter(CometTransport.STRONG_NAME_PARAMETER);
if (permutationStrongName == null) {
return null;
}
ClientOracle toReturn;
synchronized (clientOracleCache) {
if (clientOracleCache.containsKey(permutationStrongName)) {
toReturn = clientOracleCache.get(permutationStrongName).get();
if (toReturn != null) {
return toReturn;
}
}
if ("HostedMode".equals(permutationStrongName)) {
// if (!allowHostedModeConnections()) {
// throw new SecurityException("Blocked hosted mode request");
// }
toReturn = new HostedModeClientOracle();
}
else {
String moduleBase = request.getParameter(CometTransport.MODULE_BASE_PARAMETER);
if (moduleBase == null) {
return null;
}
String basePath = new URL(moduleBase).getPath();
if (basePath == null) {
throw new MalformedURLException("Blocked request without GWT base path parameter (XSRF attack?)");
}
String contextPath = getServletContext().getContextPath();
if (!basePath.startsWith(contextPath)) {
throw new MalformedURLException("Blocked request with invalid GWT base path parameter (XSRF attack?)");
}
basePath = basePath.substring(contextPath.length());
InputStream in = findClientOracleData(basePath, permutationStrongName);
toReturn = WebModeClientOracle.load(in);
}
clientOracleCache.put(permutationStrongName, new SoftReference<ClientOracle>(toReturn));
}
return toReturn;
}
protected static final String CLIENT_ORACLE_EXTENSION = ".gwt.rpc";
protected InputStream findClientOracleData(String requestModuleBasePath, String permutationStrongName) throws IOException {
String resourcePath = requestModuleBasePath + permutationStrongName + CLIENT_ORACLE_EXTENSION;
InputStream in = getServletContext().getResourceAsStream(resourcePath);
if (in == null) {
throw new IOException("Could not find ClientOracle data for permutation " + permutationStrongName);
}
return in;
}
public static CometSession getCometSession(HttpSession httpSession) {
return getCometSession(httpSession, new ConcurrentLinkedQueue<Serializable>());
}
public static CometSession getCometSession(HttpSession httpSession, Queue<Serializable> queue) {
return getCometSession(httpSession, true, queue);
}
public static CometSession getCometSession(HttpSession httpSession, boolean create) {
return getCometSession(httpSession, create, create ? new ConcurrentLinkedQueue<Serializable>() : null);
}
public static CometSession getCometSession(HttpSession httpSession, boolean create, Queue<Serializable> queue) {
synchronized (httpSession) {
CometSession session = (CometSession) httpSession.getAttribute(CometSession.HTTP_SESSION_KEY);
if (session == null) {
if (create) {
session = new CometSessionImpl(httpSession, queue, AsyncServlet.initialize(httpSession.getServletContext()));
httpSession.setAttribute(CometSession.HTTP_SESSION_KEY, session);
}
}
return session;
}
}
}