/*
* Copyright 2013 The Netty Project
*
* The Netty Project licenses this file to you 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.jboss.aerogear.io.netty.handler.codec.sockjs.handler;
import io.netty.channel.ChannelHandlerContext;
import org.jboss.aerogear.io.netty.handler.codec.sockjs.SockJsConfig;
import org.jboss.aerogear.io.netty.handler.codec.sockjs.SockJsSessionContext;
import org.jboss.aerogear.io.netty.handler.codec.sockjs.SockJsService;
import org.jboss.aerogear.io.netty.handler.codec.sockjs.handler.SessionState.State;
import io.netty.util.internal.StringUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
/**
* Represents the state of a SockJS session.
*
* Every session has a timestamp which is updated when the session is used, enabling
* sessions to be timed out which is a requirement of the SockJS specification.
*
* Every session also has a message queue which is used to store messages for session
* that at that point in time do not have an active receiver for the messages. For example,
* a polling transport might not currently have a connected polling request and the message
* would be stored until such a request is recieved.
*
* A SockJS session must be able to support concurrent interactions as
* some transports will have multiple connections accessing the same session. Taking a
* polling transport as an example again, it can have a long polling request and also a
* xhr-send request at the same time, both accessing the same session.
*/
final class SockJsSession {
private final String sessionId;
private final SockJsService service;
private final AtomicLong timestamp = new AtomicLong();
private final AtomicBoolean inuse = new AtomicBoolean();
private final ConcurrentLinkedQueue<String> messageQueue = new ConcurrentLinkedQueue<String>();
private final AtomicReference<State> state = new AtomicReference<State>(State.CONNECTING);
private final AtomicReference<ChannelHandlerContext> connectionCtx = new AtomicReference<ChannelHandlerContext>();
private final AtomicReference<ChannelHandlerContext> openCtx = new AtomicReference<ChannelHandlerContext>();
SockJsSession(final String sessionId, final SockJsService service) {
this.sessionId = sessionId;
this.service = service;
}
/**
* Returns the ChannelHandlerContext used to initially connect.
*
* @return {@code ChannelHandlerContext} the ChannelHandlerContext used establishing a connection.
*/
public ChannelHandlerContext connectionContext() {
return connectionCtx.get();
}
/**
* Sets the ChannelHandlerContext used to initially connect.
*
* @param ctx the ChannelHandlerContext used establishing a connection.
*/
public void setConnectionContext(final ChannelHandlerContext ctx) {
while (true) {
final ChannelHandlerContext oldCtx = connectionCtx.get();
if (connectionCtx.compareAndSet(oldCtx, ctx)) {
return;
}
}
}
/**
* Returns the ChannelHandlerContext used on an open session.
*
* @return {@code ChannelHandlerContext} the ChannelHandlerContext used establishing a connection.
*/
public ChannelHandlerContext openContext() {
return openCtx.get();
}
/**
* Sets the ChannelHandlerContext used to initially connect.
*
* @param ctx the ChannelHandlerContext used when the session is open.
*/
public void setOpenContext(final ChannelHandlerContext ctx) {
while (true) {
final ChannelHandlerContext oldCtx = openCtx.get();
if (openCtx.compareAndSet(oldCtx, ctx)) {
return;
}
}
}
/**
* Sets the {@link State} of this session.
*
* @param newState the state to which this session should be set.
*/
public void setState(State newState) {
while (true) {
final State oldState = state.get();
if (state.compareAndSet(oldState, newState)) {
return;
}
}
}
public State getState() {
return state.get();
}
public boolean inuse() {
return inuse.get();
}
public void setInuse(final boolean use) {
inuse.set(use);
}
public SockJsConfig config() {
return service.config();
}
public String sessionId() {
return sessionId;
}
public void onMessage(final String message) throws Exception {
service.onMessage(message);
updateTimestamp();
}
public void onOpen(final SockJsSessionContext session) {
setState(State.OPEN);
service.onOpen(session);
updateTimestamp();
}
public void onClose() {
setState(State.CLOSED);
service.onClose();
}
public void addMessage(final String message) {
messageQueue.add(message);
updateTimestamp();
}
/**
* Returns all messages that have been stored in the session.
* The messages returned, if any, will be removed from this session.
*
* @return {@code List} the messages that have been stored in this session.
*/
public List<String> getAllMessages() {
final List<String> all = new ArrayList<String>();
for (String msg; (msg = messageQueue.poll()) != null;) {
all.add(msg);
}
return all;
}
@SuppressWarnings("ManualArrayToCollectionCopy")
public void addMessages(final String[] messages) {
for (String msg: messages) {
messageQueue.add(msg);
}
}
private void updateTimestamp() {
timestamp.set(System.currentTimeMillis());
}
/**
* Returns the timestamp for when this session was last interacted with.
* The intended usage of this timestamp if to determine when a session should be discarded/closed.
*
* @return {@code long} the timestamp which was the last time this session was used.
*/
public long timestamp() {
return timestamp.get();
}
@Override
public String toString() {
return StringUtil.simpleClassName(this) + "[sessionId=" + sessionId + ", state=" + state + ']';
}
}