/**
* The MIT License
* Copyright (c) 2010 Tad Glines
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.glines.socketio.client.jre;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.util.URIUtil;
import com.glines.socketio.client.common.SocketIOConnection;
import com.glines.socketio.common.ConnectionState;
import com.glines.socketio.common.DisconnectReason;
import com.glines.socketio.common.SocketIOException;
import com.glines.socketio.server.SocketIOFrame;
public class SocketIOConnectionXHRBase implements SocketIOConnection {
private final SocketIOConnection.SocketIOConnectionListener listener;
private final String host;
private final short port;
private final String transport;
private HttpClient client;
protected String sessionId;
protected long timeout;
protected boolean timedout = false;
protected ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
protected Future<?> timeoutTask;
protected ConnectionState state = ConnectionState.CLOSED;
protected HttpExchange currentGet;
protected HttpExchange currentPost;
protected final boolean isConnectionPersistent;
protected BlockingQueue<SocketIOFrame> queue = new LinkedBlockingQueue<SocketIOFrame>();
protected AtomicBoolean doingSend = new AtomicBoolean(false);
protected AtomicLong messageId = new AtomicLong(0);
protected String closeId = null;
protected boolean disconnectWhenEmpty = false;
public SocketIOConnectionXHRBase(SocketIOConnection.SocketIOConnectionListener listener,
String host, short port, String transport, boolean isConnectionPersistent) {
this.listener = listener;
this.host = host;
this.port = port;
this.transport = transport;
this.isConnectionPersistent = isConnectionPersistent;
}
protected void startTimeoutTimer() {
clearTimeoutTimer();
if (!timedout && timeout > 0) {
timeoutTask = executor.schedule(new Runnable() {
@Override
public void run() {
SocketIOConnectionXHRBase.this.onTimeout("Inactivity Timer Timeout");
}
}, timeout, TimeUnit.MILLISECONDS);
}
}
protected void clearTimeoutTimer() {
if (timeoutTask != null) {
timeoutTask.cancel(false);
timeoutTask = null;
}
}
@Override
public void connect() {
if (state != ConnectionState.CLOSED) {
throw new IllegalStateException("Not CLOSED!");
}
state = ConnectionState.CONNECTING;
client = new HttpClient();
client.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
client.setIdleTimeout(30*1000); //30 seconds
try {
client.start();
} catch (Exception e) {
client = null;
_disconnect(DisconnectReason.CONNECT_FAILED, "Failed to initialize");
return;
}
doGet();
}
@Override
public void close() {
state = ConnectionState.CLOSING;
closeId = "" + messageId.get();
sendFrame(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, closeId));
}
@Override
public void disconnect() {
_disconnect(DisconnectReason.DISCONNECT, null);
}
protected void _disconnect(DisconnectReason reason, String error) {
switch (state) {
case CONNECTING:
listener.onDisconnect(DisconnectReason.CONNECT_FAILED, error);
break;
case CLOSED:
listener.onDisconnect(DisconnectReason.CLOSED, error);
break;
case CLOSING:
if (closeId != null) {
listener.onDisconnect(DisconnectReason.CLOSE_FAILED, error);
} else {
listener.onDisconnect(DisconnectReason.CLOSED_REMOTELY, error);
}
break;
case CONNECTED:
listener.onDisconnect(reason, error);
break;
}
state = ConnectionState.CLOSED;
if (currentGet != null) {
currentGet.cancel();
currentGet = null;
}
if (currentPost != null) {
currentPost.cancel();
currentPost = null;
}
if (client != null) {
try {
client.stop();
} catch (Exception e) {
// Ignore
}
client = null;
}
}
@Override
public ConnectionState getConnectionState() {
return state;
}
@Override
public void sendMessage(String message) throws SocketIOException {
sendMessage(SocketIOFrame.TEXT_MESSAGE_TYPE, message);
}
@Override
public void sendMessage(int messageType, String message) throws SocketIOException {
sendFrame(new SocketIOFrame(SocketIOFrame.FrameType.DATA, messageType, message));
}
protected void sendFrame(SocketIOFrame frame) {
messageId.incrementAndGet();
queue.offer(frame);
checkSend();
}
protected void checkSend() {
if (doingSend.compareAndSet(false, true)) {
StringBuilder str = new StringBuilder();
Collection<SocketIOFrame> elements = new LinkedList<SocketIOFrame>();
queue.drainTo(elements);
for (SocketIOFrame frame: elements) {
str.append(frame.encode());
}
doSend(str.toString());
}
}
protected void onTimeout(String error) {
_disconnect(DisconnectReason.TIMEOUT, error);
}
protected void onData(String data) {
startTimeoutTimer();
onMessageBlock(data);
}
protected void onMessageBlock(String data) {
for (SocketIOFrame frame: SocketIOFrame.parse(data)) {
onFrame(frame);
}
}
protected void onFrame(SocketIOFrame frame) {
switch (frame.getFrameType()) {
case SESSION_ID:
if (state == ConnectionState.CONNECTING) {
sessionId = frame.getData();
state = ConnectionState.CONNECTED;
listener.onConnect();
}
break;
case HEARTBEAT_INTERVAL:
timeout = Integer.parseInt(frame.getData());
startTimeoutTimer();
break;
case CLOSE:
onClose(frame.getData());
break;
case PING:
onPing(frame.getData());
break;
case PONG:
onPong(frame.getData());
break;
case DATA:
onMessageData(frame.getMessageType(), frame.getData());
break;
default:
break;
}
}
protected void onClose(String data) {
if (state == ConnectionState.CLOSING) {
if (closeId != null && closeId.equals(data)) {
state = ConnectionState.CLOSED;
_disconnect(DisconnectReason.DISCONNECT, null);
}
} else {
state = ConnectionState.CLOSING;
disconnectWhenEmpty = true;
sendFrame(new SocketIOFrame(SocketIOFrame.FrameType.CLOSE, 0, data));
}
}
protected void onPing(String data) {
sendFrame(new SocketIOFrame(SocketIOFrame.FrameType.PONG, 0, data));
}
protected void onPong(String data) {
// Ignore
}
protected void onMessageData(int messageType, String data) {
if (messageType == SocketIOFrame.TEXT_MESSAGE_TYPE) {
listener.onMessage(messageType, data);
}
}
protected String getRequestURI() {
if (sessionId != null) {
return "/socket.io/" + transport + "/" + sessionId + "/" + System.currentTimeMillis();
} else {
return "/socket.io/" + transport + "//" + System.currentTimeMillis();
}
}
protected String getPostURI() {
return getRequestURI() + "/send";
}
protected void doGet() {
HttpExchange exch = new HttpExchange() {
@Override
protected void onConnectionFailed(Throwable x) {
currentGet = null;
_disconnect(DisconnectReason.ERROR, x.toString());
}
@Override
protected void onException(Throwable x) {
currentGet = null;
_disconnect(DisconnectReason.ERROR, x.toString());
}
@Override
protected void onExpire() {
currentGet = null;
_disconnect(DisconnectReason.TIMEOUT, "Request timed-out");
}
@Override
protected void onResponseComplete() throws IOException {
currentGet = null;
if (!isConnectionPersistent) {
if (this.getStatus() == 200) {
if (state != ConnectionState.CLOSED) {
doGet();
}
} else {
_disconnect(DisconnectReason.ERROR, "HTTP response code: " + this.getStatus());
}
}
}
@Override
protected void onResponseContent(Buffer content) throws IOException {
onData(content.toString());
}
};
exch.setMethod("GET");
exch.setAddress(new Address(host, port));
exch.setURI(getRequestURI());
try {
client.send(exch);
currentGet = exch;
} catch (IOException e) {
_disconnect(DisconnectReason.ERROR, e.toString());
}
}
protected String getPostContentType() {
return "application/x-www-form-urlencoded; charset=utf-8";
}
protected Buffer formatPostData(String data) {
return new ByteArrayBuffer("data=" + URIUtil.encodePath(data));
}
protected void doSend(String data) {
HttpExchange exch = new HttpExchange() {
@Override
protected void onConnectionFailed(Throwable x) {
currentPost = null;
doingSend.set(false);
_disconnect(DisconnectReason.ERROR, x.toString());
}
@Override
protected void onException(Throwable x) {
currentPost = null;
doingSend.set(false);
_disconnect(DisconnectReason.ERROR, x.toString());
}
@Override
protected void onExpire() {
currentPost = null;
doingSend.set(false);
_disconnect(DisconnectReason.TIMEOUT, "Request timed-out");
}
@Override
protected void onResponseComplete() throws IOException {
currentPost = null;
if (this.getStatus() != 200) {
_disconnect(DisconnectReason.ERROR, "POST Failed with response code: " + this.getStatus());
} else {
doingSend.set(false);
checkSend();
}
}
};
exch.setMethod("GET");
exch.setRequestContentType(getPostContentType());
exch.setRequestContent(formatPostData(data));
try {
client.send(exch);
currentPost = exch;
} catch (IOException e) {
doingSend.set(false);
_disconnect(DisconnectReason.ERROR, "Failed to create SEND request");
}
}
}