fi.jasoft.remoteconnection.client.ClientRemoteConnection.java Source code

Java tutorial

Introduction

Here is the source code for fi.jasoft.remoteconnection.client.ClientRemoteConnection.java

Source

/*
* Copyright 2013 John Ahlroos
*
* 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 fi.jasoft.remoteconnection.client;

import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.ScriptInjector;
import com.google.gwt.dev.util.Util;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;

import elemental.js.util.Json;
import fi.jasoft.remoteconnection.client.peer.DataConnection;
import fi.jasoft.remoteconnection.client.peer.ObjectPeerListener;
import fi.jasoft.remoteconnection.client.peer.Peer;
import fi.jasoft.remoteconnection.client.peer.PeerError;
import fi.jasoft.remoteconnection.client.peer.PeerListener;
import fi.jasoft.remoteconnection.client.peer.PeerOptions;
import fi.jasoft.remoteconnection.client.peer.StringPeerListener;
import fi.jasoft.remoteconnection.shared.ConnectedListener;
import fi.jasoft.remoteconnection.shared.ConnectionError;
import fi.jasoft.remoteconnection.shared.IncomingChannelConnectionListener;
import fi.jasoft.remoteconnection.shared.RemoteChannel;
import fi.jasoft.remoteconnection.shared.RemoteConnection;
import fi.jasoft.remoteconnection.shared.RemoteConnectionDataListener;
import fi.jasoft.remoteconnection.shared.RemoteConnectionErrorHandler;
import fi.jasoft.remoteconnection.shared.RemoteConnectionConfiguration;

/**
 * Client side implementation of {@link RemoteConnection}. 
 * Use {@link ClientRemoteConnection#register()} to get a instance.
 * 
 * @author John Ahlroos
 */
public class ClientRemoteConnection implements RemoteConnection {

    /**
     * Client side implementation of RemoteConnection. Use {@link ClientRemoteConnection#register()} to get an instance.
     * 
     * @author John Ahlroos
     */
    public class ClientRemoteChannel implements RemoteChannel {

        private final String id;

        private DataConnection connection;

        private List<String> messageQueue = new LinkedList<String>();

        private final List<RemoteConnectionDataListener> listeners = new LinkedList<RemoteConnectionDataListener>();

        private final List<ConnectedListener> connectedListeners = new LinkedList<ConnectedListener>();

        @Override
        public void send(String message) {
            if (isConnected()) {
                ClientRemoteConnection.getLogger().info("Sending message to " + id);
                connection.send(message);
            } else {
                ClientRemoteConnection.getLogger()
                        .warning("No connection to channel endpoint. Queueing message for later.");
                messageQueue.add(message);
            }
        }

        @Override
        public String getId() {
            return id;
        }

        @Override
        public boolean isConnected() {
            return connection != null && connection.isOpen();
        }

        @Override
        public void addConnectedListener(ConnectedListener listener) {
            connectedListeners.add(listener);
        }

        /**
         * Default constructor
         * @param id
         */
        private ClientRemoteChannel(String id) {
            this.id = id;
        }

        /**
         * Adds a data listener to the channel
         * 
         * @param listener
         *       The listener to add
         */
        private void addDataListener(RemoteConnectionDataListener listener) {
            listeners.add(listener);
        }

        private void setConnection(final DataConnection con) {
            if (con == null) {
                throw new IllegalArgumentException("Connection cannot be null");
            }

            this.connection = con;

            ClientRemoteConnection.getLogger().info("Opening channel connection to " + connection.getPeerId());
            connection.addListener("open", new PeerListener() {

                @Override
                public void execute() {
                    ClientRemoteConnection.getLogger().info("Connected to channel " + getId());
                    ;
                    flushMessageQueue();
                    for (ConnectedListener listener : connectedListeners) {
                        listener.connected(getId());
                    }
                }
            });

            connection.addDataListener(new StringPeerListener() {

                @Override
                public void execute(String str) {
                    messageRecieved(str);
                }
            });
        }

        private void flushMessageQueue() {
            while (!messageQueue.isEmpty()) {
                this.send(messageQueue.remove(0));
            }
        }

        private void messageRecieved(String message) {
            for (RemoteConnectionDataListener listener : listeners) {
                listener.dataRecieved(this, message);
            }
        }
    }

    // Currently open connectedChannels
    private final List<ClientRemoteChannel> connectedChannels = new LinkedList<ClientRemoteChannel>();

    private final List<ClientRemoteChannel> pendingConnectionChannels = new LinkedList<ClientRemoteChannel>();

    private final List<RemoteConnectionDataListener> listeners = new LinkedList<RemoteConnectionDataListener>();

    private final List<IncomingChannelConnectionListener> incomingListeners = new LinkedList<IncomingChannelConnectionListener>();

    private final List<ConnectedListener> connectedListeners = new LinkedList<ConnectedListener>();

    private Peer peer;

    private boolean connectedToSignallingServer = false;

    private RemoteConnectionErrorHandler errorHandler;

    private static String PEER_JS_URL = "http://cdn.peerjs.com/0.3.4/peer.min.js";

    private boolean scriptLoaded = false;

    private boolean scriptFailedToLoad = false;

    private final RemoteConnectionConfiguration configuration;

    static Logger getLogger() {
        return Logger.getLogger(ClientRemoteConnection.class.getName());
    }

    private Timer connectionTimeoutTimer = new Timer() {

        @Override
        public void run() {
            getLogger().severe("Remote connection timed out");
        }
    };

    /**
     * Registers a new remote connection. Id is autogenerated by the signalling server.
     */
    public static RemoteConnection register() {
        return register(new RemoteConnectionConfiguration());
    }

    /**
     * Register a new Remote connection with a specific id
     * 
     * @param id
     *       The unique id of the connection
     * @return
     */
    public static RemoteConnection register(RemoteConnectionConfiguration configuration) {
        return new ClientRemoteConnection(configuration);
    }

    private ClientRemoteConnection(RemoteConnectionConfiguration configuration) {
        this.configuration = configuration;
        this.scriptLoaded = !configuration.isScriptInjected();
    }

    private final native boolean isPeerAvailable()
    /*-{
        try{
     return typeof($wnd.Peer) === 'function'; 
       } catch(e){
     return false;
       }
    }-*/;

    @Override
    public void connect() {
        if (peer != null) {
            throw new IllegalStateException("Already connected, call terminate() before connecting again");
        }

        // Inject the script if needed
        if (!scriptLoaded && !scriptFailedToLoad && !isPeerAvailable()) {
            ScriptInjector.fromUrl(PEER_JS_URL).setWindow(ScriptInjector.TOP_WINDOW)
                    .setCallback(new Callback<Void, Exception>() {

                        @Override
                        public void onSuccess(Void result) {
                            getLogger().info("Loaded peer.js successfully");
                            scriptLoaded = true;
                            if (isPeerAvailable()) {
                                connect();
                            } else {
                                getLogger().severe("Peer is not available in DOM after loading script. Aborting.");
                                scriptFailedToLoad = true;
                            }
                        }

                        @Override
                        public void onFailure(Exception reason) {
                            scriptFailedToLoad = true;
                            getLogger().severe("Failed to load Peer.js from " + PEER_JS_URL);
                        }
                    }).inject();
            return;
        }

        if (configuration.getKey().equals(RemoteConnectionConfiguration.DEVELOPMENT_PEER_JS_KEY)) {
            getLogger().warning("You are using the development key of RemoteConnection "
                    + "with a very limited amount of connections shared among all "
                    + "RemoteConnection users. You are strongly encoraged to apply "
                    + "for your own developer key at http://peerjs.com/peerserver or "
                    + "run your own server which can be downloaded from https://github.com/peers/peerjs-server. "
                    + "You can supply your own peer server details through the RemoteConnection.getConfiguration() "
                    + "option. Thank you.");
        }

        if (!isPeerAvailable()) {
            throw new IllegalStateException("Peer library is missing from DOM. Cannot connect.");
        }

        if (!scriptLoaded) {
            throw new IllegalStateException("Peer script is not loaded. Cannot connect.");
        }

        // Create peer
        try {
            peer = Peer.create(configuration.getId(), getOptionsFromConfiguration(configuration));
        } catch (Exception e) {
            throw new RuntimeException("Could not create peer connection.", e);
        }

        // Register with signaling server
        peer.addListener("open", new StringPeerListener() {

            @Override
            public void execute(String peerId) {
                connectionTimeoutTimer.cancel();
                onOpen(peerId);
            }
        });

        // Triggered when another remote connection is established
        peer.addListener("connection", new ObjectPeerListener() {

            @Override
            public void execute(JavaScriptObject obj) {
                onConnection((DataConnection) obj);
            }
        });

        // Triggered when an error occurs
        peer.addListener("error", new ObjectPeerListener() {

            @Override
            public void execute(JavaScriptObject obj) {
                connectionTimeoutTimer.cancel();
                onError((PeerError) obj);
            }
        });

        // Triggered when the connection is closed
        peer.addListener("close", new PeerListener() {

            @Override
            public void execute() {
                onClose();
            }
        });

        // Listen for timeout
        connectionTimeoutTimer.schedule(10000);
    }

    private static PeerOptions getOptionsFromConfiguration(RemoteConnectionConfiguration configuration) {
        PeerOptions options = new PeerOptions();
        options.key = configuration.getKey();
        options.port = configuration.getPort();
        options.host = configuration.getHost();
        options.secure = configuration.isSecure();
        options.config = configuration.getConfig();
        options.debug = configuration.getDebug();
        return options;
    }

    /**
     * Triggered when a connection to the signalling server has been made
     * 
     * @param peerId
     *       The peer id recieved from the signalling server
     */
    protected void onOpen(String peerId) {
        configuration.setId(peerId);
        connectedToSignallingServer = true;
        getLogger().info("Connected to signalling server. Listening on id " + configuration.getId());
        flushChannelQueue();
        for (ConnectedListener listener : connectedListeners) {
            listener.connected(peerId);
        }
    }

    /**
     * Trigged when an external connection is being requested
     * 
     * @param connection
     *       The incoming data connection
     */
    protected void onConnection(DataConnection connection) {
        ClientRemoteChannel channel = getChannelById(connection.getPeerId());
        getLogger().info("Recieved incoming connection from " + connection.getPeerId());

        if (channel == null) {
            channel = new ClientRemoteChannel(connection.getPeerId());
            connectToChannel(channel, connection);
            for (IncomingChannelConnectionListener listener : incomingListeners) {
                listener.connected(channel);
            }
        }
    }

    /**
     * Triggered when the peer is closed.
     */
    protected void onClose() {
        getLogger().info("Connection closed");
    }

    /**
     * Triggered when an error occurs with the peer
     * 
     * @param error
     *       The error that was triggered
     */
    protected void onError(PeerError error) {
        ConnectionError ce = error.getType();
        String msg = (ce == null ? new JSONObject(error).toString() : ce.toString());

        if (ce == null) {
            ce = ConnectionError.CHANNEL_ERROR;
        }

        if (errorHandler != null) {
            getLogger().severe("Remote connection got error: " + msg);
            if (errorHandler.onConnectionError(ce, msg)) {
                terminate();
            }
            ;
        } else {
            terminate();
            getLogger().severe("Remote connection terminated with the error: " + msg);

        }
    }

    /**
     * Disconnects from the signalling server but leaves all open channels open. 
     * Call {@link #terminate()} to close all open channels as well.
     */
    public void disconnect() {
        if (isPeerAvailable()) {
            peer.disconnect();
            connectedToSignallingServer = false;
        }
    }

    @Override
    public void terminate() {
        if (isPeerAvailable()) {
            disconnect();
            peer.destroy();
            peer = null;
            pendingConnectionChannels.clear();
            connectedChannels.clear();
        }
    }

    private void flushChannelQueue() {
        while (!pendingConnectionChannels.isEmpty()) {
            connectToChannel(pendingConnectionChannels.remove(0));
        }
    }

    private ClientRemoteChannel connectToChannel(ClientRemoteChannel channel) {
        getLogger().info("Connecting to peer " + channel.getId());
        DataConnection connection = peer.connect(channel.getId());
        assert connection != null;
        return connectToChannel(channel, connection);
    }

    private ClientRemoteChannel connectToChannel(ClientRemoteChannel channel, DataConnection connection) {
        channel.setConnection(connection);

        for (RemoteConnectionDataListener listener : listeners) {
            channel.addDataListener(listener);
        }

        connectedChannels.add(channel);

        return channel;
    }

    @Override
    public RemoteChannel openChannel(String endpointPeerId) {
        if (endpointPeerId == null) {
            throw new IllegalArgumentException("Cannot connect to null channel");
        }

        ClientRemoteChannel channel = getChannelById(endpointPeerId);
        if (channel != null) {
            return channel;
        }

        channel = new ClientRemoteChannel(endpointPeerId);

        getLogger().info("Created channel to " + endpointPeerId);

        if (connectedToSignallingServer) {
            channel = connectToChannel(channel);
        } else {
            getLogger().warning(
                    "Not connected to signalling server, delaying channel connection to " + endpointPeerId);
            pendingConnectionChannels.add(channel);
        }

        return channel;
    }

    @Override
    public void addDataListener(RemoteConnectionDataListener listener) {
        listeners.add(listener);
        for (ClientRemoteChannel channel : connectedChannels) {
            channel.addDataListener(listener);
        }
    }

    @Override
    public RemoteChannel getChannel(String channelEndpointId) {
        RemoteChannel channel = getChannelById(channelEndpointId);
        if (channel == null) {
            for (ClientRemoteChannel c : pendingConnectionChannels) {
                if (c.getId().equals(channelEndpointId)) {
                    channel = c;
                    break;
                }
            }
        }
        return channel;
    }

    @Override
    public void broadcast(String message) {
        for (RemoteChannel channel : connectedChannels) {
            channel.send(message);
        }
    }

    @Override
    public void setErrorHandler(RemoteConnectionErrorHandler handler) {
        this.errorHandler = handler;
    }

    /**
     * Returns, or creates a new, channel by using its remote peer id
     * 
     * @param id
     *       The peer id of the channel endpoint
     * @return
     */
    private ClientRemoteChannel getChannelById(String id) {
        for (ClientRemoteChannel channel : connectedChannels) {
            if (channel.getId().equals(id)) {
                return channel;
            }
        }
        return null;
    }

    @Override
    public boolean isConnected() {
        return peer != null && isPeerAvailable();
    }

    @Override
    public void addIncomingConnectionListener(IncomingChannelConnectionListener listener) {
        incomingListeners.add(listener);
    }

    @Override
    public void addConnectedListener(ConnectedListener listener) {
        connectedListeners.add(listener);
    }

    @Override
    public RemoteConnectionConfiguration getConfiguration() {
        return configuration;
    }
}