/*
* Copyright 2014 Sebastien Dionne
*
* 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 org.atmosphere.socketio.transport;
import org.atmosphere.cache.BroadcastMessage;
import org.atmosphere.cpr.Action;
import org.atmosphere.cpr.ApplicationConfig;
import org.atmosphere.cpr.AtmosphereHandler;
import org.atmosphere.cpr.AtmosphereRequest;
import org.atmosphere.cpr.AtmosphereResourceEvent;
import org.atmosphere.cpr.AtmosphereResourceEventListenerAdapter;
import org.atmosphere.cpr.AtmosphereResourceImpl;
import org.atmosphere.cpr.AtmosphereResponse;
import org.atmosphere.cpr.DefaultBroadcaster;
import org.atmosphere.socketio.SocketIOClosedException;
import org.atmosphere.socketio.SocketIOException;
import org.atmosphere.socketio.SocketIOPacket;
import org.atmosphere.socketio.SocketIOSession;
import org.atmosphere.socketio.SocketIOSessionFactory;
import org.atmosphere.socketio.SocketIOSessionOutbound;
import org.atmosphere.socketio.cpr.SocketIOAtmosphereHandler;
import org.atmosphere.socketio.transport.SocketIOPacketImpl.PacketType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Sebastien Dionne : sebastien.dionne@gmail.com
* @author Jeanfrancois Arcand
*/
public abstract class XHRTransport extends AbstractTransport {
private static final Logger logger = LoggerFactory.getLogger(XHRTransport.class);
private final int bufferSize;
protected abstract class XHRSessionHelper implements SocketIOSessionOutbound {
private final ReentrantLock lock = new ReentrantLock();
protected final SocketIOSession session;
private volatile boolean is_open = false;
private final boolean isStreamingConnection;
XHRSessionHelper(SocketIOSession session, boolean isConnectionPersistant) {
this.session = session;
this.isStreamingConnection = isConnectionPersistant;
}
protected abstract void startSend(AtmosphereResponse response) throws IOException;
protected abstract void writeData(AtmosphereResponse response, String data) throws IOException;
protected abstract void finishSend(AtmosphereResponse response) throws IOException;
@Override
public void disconnect() {
synchronized (this) {
session.onDisconnect(DisconnectReason.DISCONNECT);
abort();
}
}
@Override
public void close() {
synchronized (this) {
session.startClose();
}
}
@Override
public void sendMessage(SocketIOPacket packet) throws SocketIOException {
if (packet != null) {
sendMessage(packet.toString());
}
}
@Override
public void sendMessage(List<SocketIOPacketImpl> messages) throws SocketIOException {
if (messages != null) {
for (SocketIOPacketImpl msg : messages) {
switch (msg.getFrameType()) {
case MESSAGE:
case JSON:
case EVENT:
case ACK:
case ERROR:
msg.setPadding(messages.size() > 1);
try {
sendMessage(msg.toString());
} catch (Exception e) {
AtmosphereResourceImpl resource = session.getAtmosphereResourceImpl();
// if BroadcastCache is available, add the message to the cache
if (resource != null && DefaultBroadcaster.class.isAssignableFrom(resource.getBroadcaster().getClass())) {
resource.getBroadcaster().getBroadcasterConfig().getBroadcasterCache().
addToCache(resource.getBroadcaster().getID(), resource.uuid(),
new BroadcastMessage(msg));
}
}
break;
default:
logger.error("Unknown SocketIOEvent msg = " + msg);
}
}
}
}
@Override
public void sendMessage(String message) throws SocketIOException {
logger.trace("Session[" + session.getSessionId() + "]: " + "sendMessage(String): " + message);
synchronized (this) {
if (is_open) {
AtmosphereResourceImpl resource = session.getAtmosphereResourceImpl();
logger.trace("Session[" + session.getSessionId() + "]: " + resource.getRequest().getMethod() + "sendMessage");
try {
writeData(resource.getResponse(), message);
} catch (Exception e) {
if (!resource.isCancelled()) {
logger.trace("calling from " + this.getClass().getName() + " : " + "sendMessage ON FORCE UN RESUME");
try {
finishSend(resource.getResponse());
} catch (IOException ex) {
logger.trace("", ex);
}
resource.resume();
}
throw new SocketIOException(e);
}
if (!isStreamingConnection) {
try {
finishSend(resource.getResponse());
} catch (IOException e) {
logger.trace("", e);
}
resource.resume();
} else {
logger.trace("calling from " + this.getClass().getName() + " : " + "sendMessage");
session.startHeartbeatTimer();
}
} else {
logger.trace("calling from " + this.getClass().getName() + " : " + "SocketIOClosedException sendMessage");
throw new SocketIOClosedException();
}
}
}
@Override
public Action handle(AtmosphereRequest request, final AtmosphereResponse response, SocketIOSession session) throws IOException {
logger.trace("Session id[" + session.getSessionId() + "] method=" + request.getMethod() + " response HashCode=" + response.hashCode());
AtmosphereResourceImpl resource = (AtmosphereResourceImpl) request.getAttribute(ApplicationConfig.ATMOSPHERE_RESOURCE);
if ("GET".equals(request.getMethod())) {
synchronized (this) {
if (!is_open) {
response.sendError(AtmosphereResponse.SC_NOT_FOUND);
} else {
if (!isStreamingConnection) {
if (resource != null) {
resource.getRequest().setAttribute(SocketIOAtmosphereHandler.SOCKETIO_SESSION_ID, session.getSessionId());
resource.getRequest().setAttribute(SocketIOAtmosphereHandler.SOCKETIO_SESSION_OUTBOUND, session.getTransportHandler());
session.setAtmosphereResourceImpl(resource);
resource.addEventListener(new AtmosphereResourceEventListenerAdapter() {
@Override
public void onResume(AtmosphereResourceEvent event) {
if (event.isResumedOnTimeout()) {
event.getResource().write(new SocketIOPacketImpl(PacketType.NOOP).toString());
}
}
});
session.clearTimeoutTimer();
request.setAttribute(SESSION_KEY, session);
boolean resume = false;
StringBuilder data = new StringBuilder();
// if there is a Broadcaster cache, retrieve the messages from the cache, and send them
if (DefaultBroadcaster.class.isAssignableFrom(resource.getBroadcaster().getClass())) {
List<Object> cachedMessages = resource.getBroadcaster().getBroadcasterConfig().getBroadcasterCache()
.retrieveFromCache(resource.getBroadcaster().getID(), resource.uuid());
if (cachedMessages != null && !cachedMessages.isEmpty()) {
if (cachedMessages.size() > 1) {
for (Object object : cachedMessages) {
String msg = object.toString();
data.append(SocketIOPacketImpl.SOCKETIO_MSG_DELIMITER)
.append(msg.length())
.append(SocketIOPacketImpl.SOCKETIO_MSG_DELIMITER).append(msg);
}
} else if (cachedMessages.size() == 1) {
data.append(cachedMessages.get(0));
}
// something to send ?
if (!data.toString().isEmpty()) {
startSend(response);
writeData(response, data.toString());
finishSend(response);
// force a resume, because we sent data
resource.resume();
resume = true;
}
}
}
if (!resume) {
resource.disableSuspend(false);
resource.suspend(session.getRequestSuspendTime());
resource.disableSuspend(true);
}
}
} else {
// won't happend, by should be for xhr-streaming transport
response.sendError(AtmosphereResponse.SC_NOT_FOUND);
}
}
}
} else if ("POST".equals(request.getMethod())) {
if (is_open) {
int size = request.getContentLength();
if (size == 0) {
response.sendError(AtmosphereResponse.SC_BAD_REQUEST);
} else {
String data = (String) request.getAttribute(POST_MESSAGE_RECEIVED);
if (data == null) {
data = decodePostData(request.getContentType(), extractString(request.getReader()));
}
if (data == null || data.length() == 0) {
data = SocketIOSessionManagerImpl.mapper.readValue(request.getParameter("d"), String.class);
}
if (data != null && data.length() > 0) {
List<SocketIOPacketImpl> list = SocketIOPacketImpl.parse(data);
synchronized (session) {
for (SocketIOPacketImpl msg : list) {
if (msg.getFrameType().equals(SocketIOPacketImpl.PacketType.EVENT)) {
// send message on the suspended request
session.onMessage(session.getAtmosphereResourceImpl(), session.getTransportHandler(), msg.getData());
// send completion flag on the post request
writeData(response, SocketIOPacketImpl.POST_RESPONSE);
} else {
// send completion flag on the post request
writeData(response, SocketIOPacketImpl.POST_RESPONSE);
}
}
}
}
}
// force a resume on a POST request
resource.resume();
}
} else {
response.sendError(AtmosphereResponse.SC_BAD_REQUEST);
}
return Action.CANCELLED;
}
protected abstract void customConnect(AtmosphereRequest request, AtmosphereResponse response) throws IOException;
public void connect(AtmosphereResourceImpl resource) throws IOException {
AtmosphereRequest request = resource.getRequest();
AtmosphereResponse response = resource.getResponse();
request.setAttribute(SESSION_KEY, session);
response.setBufferSize(bufferSize);
customConnect(request, response);
is_open = true;
session.onConnect(resource, this);
finishSend(response);
if (isStreamingConnection) {
resource.suspend();
}
}
@Override
public void abort() {
logger.error("calling from " + this.getClass().getName() + " : " + "abort");
session.clearHeartbeatTimer();
session.clearTimeoutTimer();
is_open = false;
session.onShutdown();
// force a resume
session.getAtmosphereResourceImpl().resume();
}
@Override
public String getSessionId() {
return session.getSessionId();
}
}
public XHRTransport(int bufferSize) {
this.bufferSize = bufferSize;
}
/**
* This method should only be called within the context of an active HTTP request.
*/
protected abstract XHRSessionHelper createHelper(SocketIOSession session);
protected SocketIOSession connect(SocketIOSession session, AtmosphereResourceImpl resource,
AtmosphereHandler atmosphereHandler, SocketIOSessionFactory sessionFactory) throws IOException {
if (session == null) {
session = sessionFactory.createSession(resource, atmosphereHandler);
resource.getRequest().setAttribute(SocketIOAtmosphereHandler.SOCKETIO_SESSION_ID, session.getSessionId());
}
XHRSessionHelper handler = createHelper(session);
handler.connect(resource);
return session;
}
protected SocketIOSession connect(AtmosphereResourceImpl resource, AtmosphereHandler atmosphereHandler, SocketIOSessionFactory sessionFactory) throws IOException {
return connect(null, resource, atmosphereHandler, sessionFactory);
}
@Override
public Action handle(AtmosphereResourceImpl resource, AtmosphereHandler atmosphereHandler, SocketIOSessionFactory sessionFactory) throws IOException {
AtmosphereRequest request = resource.getRequest();
AtmosphereResponse response = resource.getResponse();
Object obj = request.getAttribute(SESSION_KEY);
SocketIOSession session = null;
String sessionId = null;
if (obj != null) {
session = (SocketIOSession) obj;
} else {
sessionId = extractSessionId(request);
if (sessionId != null && sessionId.length() > 0) {
session = sessionFactory.getSession(sessionId);
}
}
boolean isDisconnectRequest = isDisconnectRequest(request);
Action action = Action.CONTINUE;
if (session != null) {
SocketIOSessionOutbound handler = session.getTransportHandler();
if (handler != null) {
if (!isDisconnectRequest) {
action = handler.handle(request, response, session);
} else {
handler.disconnect();
response.setStatus(200);
}
} else {
if (!isDisconnectRequest) {
// handle the Connect
session = connect(session, resource, atmosphereHandler, sessionFactory);
if (session == null) {
response.sendError(AtmosphereResponse.SC_SERVICE_UNAVAILABLE);
}
} else {
response.setStatus(200);
}
}
} else if (sessionId != null && sessionId.length() > 0) {
logger.trace("Session NULL but not sessionId : wrong session id or the connection was DISCONNECTED");
response.sendError(AtmosphereResponse.SC_BAD_REQUEST);
} else {
if ("GET".equals(request.getMethod())) {
session = connect(resource, atmosphereHandler, sessionFactory);
if (session == null) {
response.sendError(AtmosphereResponse.SC_SERVICE_UNAVAILABLE);
}
} else {
response.sendError(AtmosphereResponse.SC_BAD_REQUEST);
}
}
return action;
}
}