In our
GWT web application we use
gwt-comet library for Comet support. Unfortunately, it is not reliable under IE. For some reason it gets stuck. So, the solution was to implement custom
CometTransport for IE using
XMLHttpRequest object.
While,
gwt-comet contains
CometTransport implementation based on
XMLHttpRequest (
HTTPRequestCometTransport),
IEHTMLFileCometTransport is designed specifically for IE.
HTTPRequestCometTransport doesn't work for IE, because
XMLHttpRequest behavior is different from that expected in
HTTPRequestCometTransport. Particularly,
HTTPRequestCometTransport expects partial content loading and handling through
onReadyStateChange event delivering with
readyState = 3 (LOADING). IE doesn't support this, i.e. it doesn't return partially loaded content, but rather empty string.
Our custom implementation doesn't use partial loading. Once event is sent to the client, the connection is terminated. Then the transport initiates connection again waiting for ongoing messages.
There was also need to patch server side protocol. So, we have forced to override
CometServlet class in order to create custom servlet response object -
IERequestCometServletResponse. Below is a code:
IECometTransport.java
package com.j2start.webapp.client.comet;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.regexp.shared.SplitResult;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.rpc.StatusCodeException;
import com.google.gwt.xhr.client.XMLHttpRequest;
import com.google.gwt.xhr.client.ReadyStateChangeHandler;
import net.zschech.gwt.comet.client.impl.RawDataCometTransport;
/**
*
* @author y_plaksyuk
*/
public class IECometTransport extends RawDataCometTransport {
private static final String SEPARATOR = "\n";
private static RegExp separator;
private boolean connected = false;
static {
Event.addNativePreviewHandler(new NativePreviewHandler() {
@Override
public void onPreviewNativeEvent(NativePreviewEvent e) {
if (e.getTypeInt() == Event.getTypeInt(KeyDownEvent.getType().getName())) {
NativeEvent nativeEvent = e.getNativeEvent();
if (nativeEvent.getKeyCode() == KeyCodes.KEY_ESCAPE) {
nativeEvent.preventDefault();
}
}
}
});
separator = RegExp.compile(SEPARATOR);
}
private XMLHttpRequest xmlHttpRequest;
@Override
public void connect(int connectionCount) {
super.connect(connectionCount);
xmlHttpRequest = XMLHttpRequest.create();
try {
xmlHttpRequest.open("GET", getUrl(connectionCount));
xmlHttpRequest.setRequestHeader("Accept", "application/comet+ie");
xmlHttpRequest.setOnReadyStateChange(new ReadyStateChangeHandler() {
@Override
public void onReadyStateChange(XMLHttpRequest request) {
if (!disconnecting) {
if (!connected) {
onReceiving(Response.SC_OK, "!15000\n"); // TODO: hardcoded default value
connected = true;
}
if (request.getReadyState() == XMLHttpRequest.DONE)
onLoaded(request.getStatus(), request.getResponseText());
}
}
});
xmlHttpRequest.send();
}
catch (JavaScriptException e) {
cleanupHttpRequest(false);
listener.onError(new RequestException(e.getMessage()), false);
}
}
@Override
public void disconnect() {
super.disconnect();
cleanupHttpRequest(true);
}
private void onLoaded(int statusCode, String responseText) {
onReceiving(statusCode, responseText, false);
}
private void onReceiving(int statusCode, String responseText) {
onReceiving(statusCode, responseText, true);
}
private void onReceiving(int statusCode, String responseText, boolean connected) {
if (!connected)
cleanupHttpRequest(false);
if (statusCode != Response.SC_OK) {
if (!connected) {
super.disconnect();
listener.onError(new StatusCodeException(statusCode, responseText), connected);
}
}
else {
List<serializable> messages = new ArrayList<serializable>();
SplitResult data = separator.split(responseText);
int length = data.length();
for (int i = 0; i < length; i++) {
if (disconnecting) {
return;
}
String message = data.get(i);
if (!message.isEmpty()) {
parse(message, messages);
}
}
if (!messages.isEmpty())
listener.onMessage(messages);
if (!connected) {
super.disconnect();
super.disconnected();
}
}
}
private void cleanupHttpRequest(boolean abort) {
if (xmlHttpRequest != null) {
if (abort)
xmlHttpRequest.abort();
xmlHttpRequest.clearOnReadyStateChange();
xmlHttpRequest = null;
}
connected = false;
}
}
IERequestCometServletResponse.java
package com.j2start.webapp.server.comet;
import java.util.List;
import java.io.IOException;
import java.io.Serializable;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.gwt.rpc.server.ClientOracle;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import net.zschech.gwt.comet.server.impl.AsyncServlet;
import net.zschech.gwt.comet.server.impl.HTTPRequestCometServletResponse;
/**
*
* @author y_plaksyuk
*/
public class IERequestCometServletResponse extends HTTPRequestCometServletResponse {
public IERequestCometServletResponse(
HttpServletRequest request,
HttpServletResponse response,
SerializationPolicy serializationPolicy,
ClientOracle clientOracle,
CometServlet servlet,
AsyncServlet async,
int heartbeat) {
super(request, response, serializationPolicy, clientOracle, servlet, async, heartbeat);
}
@Override
protected void doInitiate(int heartbeat) throws IOException {
// client will imitate receiving 'initiate' command itself
}
@Override
protected void doTerminate() throws IOException {
// don't send anything, in some cases write stream is already closed
}
////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public synchronized void write(List messages, boolean flush) throws IOException {
super.write(messages, flush);
flushAndTerminate();
}
@Override
public synchronized void heartbeat() throws IOException {
super.heartbeat();
flushAndTerminate();
}
private void flushAndTerminate() throws IOException {
writer.flush();
writer.close();
OutputStream os = getAsyncOutputStream();
os.flush();
os.close();
try {
terminate();
}
catch (IOException ex) {
}
}
}
CometServlet.java
package com.j2start.webapp.server.comet;
import java.io.IOException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.gwt.rpc.server.ClientOracle;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import net.zschech.gwt.comet.server.impl.AsyncServlet;
import net.zschech.gwt.comet.server.impl.CometServletResponseImpl;
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;
/**
*
* @author y_plaksyuk
*/
public class CometServlet extends net.zschech.gwt.comet.server.CometServlet {
private transient AsyncServlet async;
@Override
public void init() throws ServletException {
ServletConfig servletConfig = getServletConfig();
String heartbeatValue = servletConfig.getInitParameter("heartbeat");
if (heartbeatValue != null)
setHeartbeat(Integer.parseInt(heartbeatValue));
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 ex) {
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+ie".equals(accept)) {
return new IERequestCometServletResponse(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();
// 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());
}
try {
// 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();
}
catch (Exception ex) {
log("Error calling response.suspend(): " + ex.getMessage());
// response.terminate();
}
}
}
You also need to update your GWT module XML file:
...
And substitute CometServlet with our own implementation in
web.xml:
30
index.html
Listener for shutting down the comet processor when the ServletContext is destroyed.
net.zschech.gwt.comet.server.CometServletContextListener
Listener for invalidating CometSessions when HTTPSessions are invalidated.
net.zschech.gwt.comet.server.CometHttpSessionListener
cometServlet
com.j2start.webapp.server.comet.CometServlet
cometServlet
/com.j2start.webapp.Main/comet