/* * 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.impl; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.NotSerializableException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.util.Collections; import java.util.List; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.atomic.AtomicBoolean; import javax.servlet.ServletResponse; import javax.servlet.ServletResponseWrapper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import net.zschech.gwt.comet.server.CometServlet; import net.zschech.gwt.comet.server.CometServletResponse; import net.zschech.gwt.comet.server.CometSession; import net.zschech.gwt.comet.server.deflate.DeflaterOutputStream; import com.google.gwt.rpc.server.ClientOracle; import com.google.gwt.rpc.server.RPC; import com.google.gwt.user.client.rpc.SerializationException; import com.google.gwt.user.server.rpc.SerializationPolicy; import com.google.gwt.user.server.rpc.impl.ServerSerializationStreamWriter; public abstract class CometServletResponseImpl implements CometServletResponse { private final HttpServletRequest request; private final HttpServletResponse response; private CometSessionImpl session; private final SerializationPolicy serializationPolicy; private final ClientOracle clientOracle; private final CometServlet servlet; private final AsyncServlet async; private final int heartbeat; private OutputStream asyncOutputStream; protected Writer writer; private boolean terminated; private boolean suspended; private AtomicBoolean processing = new AtomicBoolean(); private Object suspendInfo; private volatile long lastWriteTime; private ScheduledFuture<?> heartbeatFuture; private ScheduledFuture<?> sessionKeepAliveFuture; protected CometServletResponseImpl(HttpServletRequest request, HttpServletResponse response, SerializationPolicy serializationPolicy, ClientOracle clientOracle, CometServlet servlet, AsyncServlet async, int heartbeat) { this.request = request; this.response = response; this.serializationPolicy = serializationPolicy; this.clientOracle = clientOracle; this.servlet = servlet; this.async = async; this.heartbeat = heartbeat; } @Override public int getHeartbeat() { return heartbeat; } @Override public synchronized boolean isTerminated() { return terminated; } protected OutputStream getOutputStream(OutputStream outputStream) { return outputStream; } protected OutputStream getAsyncOutputStream() { return asyncOutputStream; } protected boolean isDeRPC() { return clientOracle != null; } @Override public HttpServletRequest getRequest() { return request; } @Override public HttpServletResponse getResponse() { return response; } protected HttpServletResponse getUnwrappedResponse() { ServletResponse result = response; while (result instanceof ServletResponseWrapper) { result = ((ServletResponseWrapper) result).getResponse(); } return (HttpServletResponse) result; } CometSessionImpl getSessionImpl() { return session; } @Override public CometSession getSession() { return getSession(true); } @Override public synchronized CometSession getSession(boolean create) { if (suspended) { throw new IllegalStateException("CometSession can not be accessed after the CometServletResponse has been suspended."); } if (session != null) { return session; } HttpSession httpSession = getRequest().getSession(create); if (httpSession == null) { return null; } session = (CometSessionImpl) CometServlet.getCometSession(httpSession, create); if (create) { session.setLastAccessedTime(); scheduleSessionKeepAlive(); session.setResponse(this); } return session; } boolean hasSession() { assert Thread.holdsLock(this); return session != null; } synchronized void scheduleSessionKeepAlive() { if (sessionKeepAliveFuture != null) { sessionKeepAliveFuture.cancel(false); } sessionKeepAliveFuture = async.scheduleSessionKeepAlive(this, session); } void scheduleHeartbeat() { assert Thread.holdsLock(this); lastWriteTime = System.currentTimeMillis(); if (heartbeatFuture != null) { heartbeatFuture.cancel(false); } heartbeatFuture = async.scheduleHeartbeat(this, session); } @Override public void sendError(int statusCode) throws IOException { sendError(statusCode, null); } @Override public synchronized void sendError(int statusCode, String message) throws IOException { if (suspended) { throw new IllegalStateException("sendError can not be accessed after the CometServletResponse has been suspended."); } try { response.reset(); setupHeaders(response); OutputStream outputStream = response.getOutputStream(); writer = new OutputStreamWriter(outputStream, "UTF-8"); doSendError(statusCode, message); } catch (IllegalStateException e) { servlet.log("Error resetting response to send error: " + e.getMessage()); } finally { setTerminated(true); } } public synchronized void initiate() throws IOException { setupHeaders(response); OutputStream outputStream = response.getOutputStream(); asyncOutputStream = outputStream = async.getOutputStream(outputStream); String acceptEncoding = request.getHeader("Accept-Encoding"); if (acceptEncoding != null && acceptEncoding.contains("deflate")) { response.setHeader("Content-Encoding", "deflate"); outputStream = new DeflaterOutputStream(outputStream); } writer = new OutputStreamWriter(getOutputStream(outputStream), "UTF-8"); scheduleHeartbeat(); getSession(false); if (session != null) { session.setLastAccessedTime(); scheduleSessionKeepAlive(); // This must be as the last step of initialise because after this // response is set in the session // it must be fully setup as it can be immediately terminated by the // next response CometServletResponseImpl prevResponse = session.setResponse(this); if (prevResponse != null) { prevResponse.tryTerminate(); } } doInitiate(heartbeat); } protected void setupHeaders(HttpServletResponse response) { response.setHeader("Cache-Control", "no-cache"); } public void suspend() { try { CometSessionImpl s; synchronized (this) { if (terminated) { return; } doSuspend(); s = session; boolean flush; if (s == null) { flush = true; } else { flush = s.isEmpty(); } if (flush) { flush(); } suspended = true; if (!(async instanceof BlockingAsyncServlet)) { suspendInfo = async.suspend(this, s, request); } } if (async instanceof BlockingAsyncServlet) { async.suspend(this, s, request); } } catch (IOException e) { servlet.log("Error suspending response", e); synchronized (this) { suspended = false; setTerminated(false); } } } synchronized Object getSuspendInfo() { return suspendInfo; } @Override public synchronized void terminate() throws IOException { if (!terminated) { try { doTerminate(); flush(); } finally { setTerminated(true); } } } void tryTerminate() { try { terminate(); } catch (IOException e) { servlet.log("Error terminating response", e); } } @Override public void write(Serializable message) throws IOException { write(Collections.singletonList(message), true); } @Override public void write(Serializable message, boolean flush) throws IOException { write(Collections.singletonList(message), flush); } @Override public void write(List<? extends Serializable> messages) throws IOException { write(messages, true); } @Override public synchronized void write(List<? extends Serializable> messages, boolean flush) throws IOException { if (terminated) { throw new IOException("CometServletResponse terminated"); } try { doWrite(messages); if (flush) { flush(); } scheduleHeartbeat(); } catch (IOException e) { setTerminated(false); throw e; } } @Override public synchronized void heartbeat() throws IOException { if (!terminated) { try { doHeartbeat(); flush(); scheduleHeartbeat(); } catch (IOException e) { setTerminated(false); throw e; } } } void tryHeartbeat() { try { heartbeat(); } catch (IOException e) { servlet.log("Error sending heartbeat", e); } } void flush() throws IOException { assert Thread.holdsLock(this); writer.flush(); } void setTerminated(boolean serverInitiated) { assert Thread.holdsLock(this); terminated = true; if (heartbeatFuture != null) { heartbeatFuture.cancel(false); heartbeatFuture = null; } if (serverInitiated) { try { writer.close(); } catch (IOException e) { servlet.log("Error closing connection", e); } } if (session != null) { session.clearResponse(this); if (sessionKeepAliveFuture != null) { sessionKeepAliveFuture.cancel(false); } } if (suspended) { async.terminate(this, session, serverInitiated, suspendInfo); } servlet.cometTerminated(this, serverInitiated); } long getHeartbeatScheduleTime() throws IllegalStateException { return heartbeat - (System.currentTimeMillis() - lastWriteTime); } protected abstract void doInitiate(int heartbeat) throws IOException; protected abstract void doSendError(int statusCode, String message) throws IOException; protected abstract void doSuspend() throws IOException; protected abstract void doWrite(List<? extends Serializable> messages) throws IOException; protected abstract void doHeartbeat() throws IOException; protected abstract void doTerminate() throws IOException; protected String serialize(Serializable message) throws NotSerializableException, UnsupportedEncodingException { try { if (clientOracle == null) { ServerSerializationStreamWriter streamWriter = new ServerSerializationStreamWriter(serializationPolicy); streamWriter.prepareToWrite(); streamWriter.writeObject(message); return streamWriter.toString(); } else { ByteArrayOutputStream result = new ByteArrayOutputStream(); RPC.streamResponseForSuccess(clientOracle, result, message); return new String(result.toByteArray(), "UTF-8"); } } catch (SerializationException e) { throw new NotSerializableException("Unable to serialize object, message: " + e.getMessage()); } } boolean setProcessing(boolean processing) { return this.processing.compareAndSet(!processing, processing); } }