/*
* 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;
}
}