com.kk_electronic.kkportal.core.rpc.Comet.java Source code

Java tutorial

Introduction

Here is the source code for com.kk_electronic.kkportal.core.rpc.Comet.java

Source

/*
 * Copyright 2010 kk-electronic a/s. 
 * 
 * This file is part of KKPortal.
 *
 * KKPortal is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * KKPortal is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with KKPortal.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.kk_electronic.kkportal.core.rpc;

import java.util.Date;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.OpenHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.kk_electronic.kkportal.core.event.FrameReceivedEvent;
import com.kk_electronic.kkportal.core.event.FrameReceivedHandler;
import com.kk_electronic.kkportal.core.event.FrameSentEvent;
import com.kk_electronic.kkportal.core.event.FrameSentHandler;

/**
 * The comet class tries to emulate the a socket connection to the server by doing long polling.
 * You should only ever use one of such class due to limitations in the number of Http requests
 * a client can make to the server.
 * As per RFC2616: "A single-user client SHOULD NOT maintain more than 2 connections with any 
 * server or proxy.".
 * The real limit is somewhat varying but designing the protocol should only rely on two.
 * Long pooling is made with a http request to the server that gets delayed on the server side
 * until a response from the server is made. Then the server closes the connection immediately
 * due to the fact some proxies only send the repsonse back to the client after the server
 * closes the connection, so the only real way to flush is by closing the connection.
 * 
 * Transmission from client to server is done using the second available request. 
 * 
 * To cope with with the limit of connections only one such frame can be sent at time. This can
 * usually be coped with by bundling like seen in the {@link RpcDispatcher}. This is true for 
 * both directions.
 * 
 * The interface to the comet class is made to be as similiar to a normal websocket so a smooth
 * transition can be made sometime in the future.
 * @author Jes Andersen
 *
 */

@Singleton
public class Comet implements WebSocket {

    private RequestCallback connectCallback = new RequestCallback() {
        @Override
        public void onError(Request request, Throwable exception) {
            status = WebSocketStatus.CLOSED;
            GWT.log("SOCKET-Could not open connection to portalserver", exception);
            CloseEvent.fire(Comet.this, Comet.this);
        }

        @Override
        public void onResponseReceived(Request request, Response response) {
            switch (response.getStatusCode()) {
            case Response.SC_CREATED:
                status = WebSocketStatus.OPEN;
                rxUrl = response.getText();
                GWT.log("SOCKET-Connection to portalserver established");
                OpenEvent.fire(Comet.this, Comet.this);
                poll();
                break;
            default:
                onError(request, new Exception("Unknown status code returned from portalserver"));
                break;
            }
        }
    };

    private RequestCallback txCallback = new RequestCallback() {
        @Override
        public void onError(Request request, Throwable exception) {
            GWT.log("SOCKET-Failed to send requests to portalserver", exception);
        }

        @Override
        public void onResponseReceived(Request request, Response response) {
            txRequest = null;
            switch (response.getStatusCode()) {
            case Response.SC_CREATED:
            case Response.SC_ACCEPTED:
                rxUrl = response.getText();
                break;
            default:
                onError(request, new Exception("Unknown Status Code returned from portalserver"));
            }
            FrameSentEvent.fire(Comet.this);
        }
    };

    private RequestCallback rxCallback = new RequestCallback() {

        @Override
        public void onError(Request request, Throwable exception) {
            GWT.log("SOCKET-Failure during communication with portalserver", exception);
            close();
        }

        @Override
        public void onResponseReceived(Request request, Response response) {
            switch (response.getStatusCode()) {
            case Response.SC_OK:
                GWT.log("SOCKET-portalserver receiving @" + new Date().getTime() + " : " + response.getText());
                FrameReceivedEvent.fire(Comet.this, response.getText());
                poll();
                break;
            case 0:
            case Response.SC_GONE:
                GWT.log("SOCKET-Lost connection to portalserver");
                close();
                break;
            default:
                onError(request,
                        new Exception("Unknown status code returned from portal server when receiving responses"));
                break;
            }
        }
    };

    private final HandlerManager eventBus;
    /**
     * We simulate the status of the connection to the server.
     */
    WebSocketStatus status = WebSocketStatus.CLOSED;
    /**
     * This is the relative url to get messages from the server on
     */
    protected String rxUrl;

    Request txRequest;

    private String url;

    private void poll() {
        if (!status.equals(WebSocketStatus.OPEN))
            return;
        RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url + rxUrl);
        try {
            builder.sendRequest(null, rxCallback);
        } catch (RequestException e) {
            GWT.log("SOCKET-Failed to get responses to portalserver", e);
        }
    }

    /**
     * Creates the class with no sideeffects.
     * @param eventBus the eventbus to send {@link FrameReceivedEvent}, {@link FrameSentEvent}, {@link OpenEvent} and {@link CloseEvent} on.
     */
    @Inject
    public Comet(HandlerManager eventBus) {
        this.eventBus = eventBus;
    }

    /**
     * listen to the connection lost messages
     */
    @Override
    public HandlerRegistration addCloseHandler(CloseHandler<WebSocket> handler) {
        return eventBus.addHandler(CloseEvent.getType(), handler);
    }

    /**
     * listen to incoming frames
     */
    @Override
    public HandlerRegistration addFrameReceivedHandler(FrameReceivedHandler handler) {
        return eventBus.addHandler(FrameReceivedEvent.getType(), handler);
    }

    /**
     * listen to frames successfully sent.
     * This is most useful for if you have a queue for frame for transmission since
     * only one frame can be transmitting at a time. 
     */
    @Override
    public HandlerRegistration addFrameSentHandler(FrameSentHandler handler) {
        return eventBus.addHandler(FrameSentEvent.getType(), handler);
    }

    /**
     * listen to when connections have actually been made.
     */
    @Override
    public HandlerRegistration addOpenHandler(OpenHandler<WebSocket> handler) {
        return eventBus.addHandler(OpenEvent.getType(), handler);
    }

    /**
     * Closes the current connection
     */
    @Override
    public void close() {
        //TODO: Abort tx and rx
        status = WebSocketStatus.CLOSED;
        rxUrl = null;
        CloseEvent.fire(this, this);
    }

    /**
     * The connect call POST an empty request to that and expects to receive a HTTP 201 Created
     * with the url of where it can receive server frames.
     * @param url when opening a connection this url is used
     * @param subprotocol not used yet, mostly here for compatibility with websocket protocol
     */
    @Override
    public void connect(String url, String subprotocol) {
        if (status.equals(WebSocketStatus.CLOSED)) {
            this.url = url;
            RequestBuilder builder = new RequestBuilder(RequestBuilder.POST, url);
            try {
                /*
                 * for now we post not an real empty request but the json variant.
                 * TODO: change the server so it can accept an empty body
                 */
                builder.sendRequest("[]", connectCallback);
            } catch (RequestException e) {
                GWT.log("SOCKET-Failed to connect to portalserver", e);
            }
            this.status = WebSocketStatus.CONNECTING;
        }
    }

    @Override
    public void fireEvent(GwtEvent<?> event) {
        eventBus.fireEvent(event);
    }

    @Override
    public boolean isConnected() {
        return status.equals(WebSocketStatus.OPEN);
    }

    /**
     * is currently transmitting a message so send() should not be called until this returns
     * false. add a {@link Comet#addFrameSentHandler(FrameSentHandler)} for a efficient
     * way of knowning when it is possible to send again.
     */
    @Override
    public boolean isTxBusy() {
        return txRequest != null;
    }

    /**
     * send a frame to the server. should not be called if {@link Comet#isTxBusy()} returns
     * true, since it this creates too many connections to the server.
     */
    @Override
    public void send(String s) {
        if (!status.equals(WebSocketStatus.OPEN))
            return;
        RequestBuilder builder = new RequestBuilder(RequestBuilder.POST, url + rxUrl);
        try {
            builder.sendRequest(s, txCallback);
        } catch (RequestException e) {
            GWT.log("SOCKET-Failed to send requests to portalserver", e);
        }
        GWT.log("SOCKET-portalserver sending @" + new Date().getTime() + " : " + s);
        FrameSentEvent.fire(this);
    }
}