Dec 25, 2011

Custom CometTransport implementation for IE

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
    


0 comments: