package ecologylab.oodss.distributed.server; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SelectionKey; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.util.Arrays; import org.java_websocket.WebSocket; import org.java_websocket.WebSocketImpl; import ecologylab.collections.Scope; import ecologylab.generic.HashMapArrayList; import ecologylab.oodss.distributed.common.ServerConstants; import ecologylab.oodss.distributed.common.SessionObjects; import ecologylab.oodss.distributed.impl.Manager; import ecologylab.oodss.distributed.impl.WebSocketServerImpl; import ecologylab.oodss.distributed.server.clientsessionmanager.SessionHandle; import ecologylab.oodss.distributed.server.clientsessionmanager.WebSocketClientSessionManager; import ecologylab.oodss.exceptions.BadClientException; import ecologylab.oodss.messages.ResponseMessage; import ecologylab.oodss.messages.ServiceMessage; import ecologylab.oodss.messages.UpdateMessage; import ecologylab.serialization.SIMPLTranslationException; import ecologylab.serialization.SimplTypesScope; import ecologylab.serialization.formatenums.StringFormat; /** * An OODSS server based on websocket * * Subclasses should generally override the generateContextManager hook method, so that they can use * their own, specific ContextManager in place of the default. * @author shenfeng */ public class WebSocketOodssServer extends Manager implements Runnable, WebSocketServerProcessor, ServerConstants { Thread t = null; boolean running = false; protected SimplTypesScope translationScope; protected Scope applicationObjectScope; public static final int DEFAULT_PORT = 2018; private WebSocketServerImpl webSocketServer; private String currentMessage; /** * Map in which keys are sessionTokens, and values are associated ClientSessionManagers */ private HashMapArrayList<Object, WebSocketClientSessionManager> clientSessionManagerMap = new HashMapArrayList<Object, WebSocketClientSessionManager>(); /** * Map in which keys are sessionTokens, and values are associated SessionHandles */ private HashMapArrayList<Object, SessionHandle> clientSessionHandleMap = new HashMapArrayList<Object, SessionHandle>(); private static final Charset ENCODED_CHARSET = Charset.forName(CHARACTER_ENCODING); private static CharsetDecoder DECODER = ENCODED_CHARSET.newDecoder(); public WebSocketOodssServer(SimplTypesScope serverTranslationScope, Scope applicationObjectScope) throws UnknownHostException { this(new InetSocketAddress(DEFAULT_PORT), serverTranslationScope, applicationObjectScope); } public WebSocketOodssServer(int port, SimplTypesScope serverTranslationScope, Scope applicationObjectScope) throws UnknownHostException { this(new InetSocketAddress(port), serverTranslationScope, applicationObjectScope); } public WebSocketOodssServer(InetSocketAddress address, SimplTypesScope serverTranslationScope, Scope applicationObjectScope) throws UnknownHostException { this.webSocketServer = new WebSocketServerImpl(address, this); this.applicationObjectScope = applicationObjectScope; this.translationScope = serverTranslationScope; applicationObjectScope.put(SessionObjects.SESSIONS_MAP, clientSessionHandleMap); applicationObjectScope.put(SessionObjects.OODSS_WEBSOCKET_SERVER, this); } /** * @return the global scope for this server */ public Scope getGlobalScope() { return applicationObjectScope; } /** * @return the translationScope */ public SimplTypesScope getTranslationSpace() { return translationScope; } @Override public void start() { debug("Server starting."); running = true; webSocketServer.start(); if (t == null) { t = new Thread(this); } t.start(); } @Override public void stop() { debug("Server stopping."); running = false; synchronized(this) { this.notify(); synchronized(t) { t = null; } } } @Override protected void shutdownImpl() { // TODO Auto-generated method stub } @Override public void run() { // TODO Auto-generated method stub } public boolean isRunning() { return running; } /** * Attempt to invalidate sessions that are permanently disconnected * * @param sessionId * @param forcePermanent * @return true if the session is invalidated. false if not */ @Override public boolean invalidate(String sessionId, boolean forcePermanent) { WebSocketClientSessionManager cm = clientSessionManagerMap.get(sessionId); // figure out if the disconnect is permanent; will be permanent if forcing // (usually bad client), if there is no context manager (client never sent // data), or if the client manager says it is invalidating (client // disconnected properly) boolean permanent = (forcePermanent ? true : (cm == null ? true : cm.isInvalidating())); // get the context manager... if (permanent) { synchronized (clientSessionManagerMap) { // ...if this session will not be restored, remove the context // manager clientSessionManagerMap.remove(sessionId); clientSessionHandleMap.remove(sessionId); } } if (cm != null) { /* * if we've gotten here, then the client has disconnected already, no reason to deal w/ the * remaining messages // finish what the context manager was working on while * (cm.isMessageWaiting()) { try { cm.processAllMessagesAndSendResponses(); } catch * (BadClientException e) { e.printStackTrace(); } } */ cm.shutdown(); } return permanent; } /** * Attempts to switch the ContextManager for a SocketChannel. oldId indicates the session id that * was used for the connection previously (in order to find the correct ContextManager) and * newContextManager is the recently-created (and now, no longer necessary) ContextManager for the * connection. * * @param oldSessionId * @param newSessionManager * @return true if the restore was successful, false if it was not. */ @Override public boolean restoreContextManagerFromSessionId(String oldSessionId, WebSocketClientSessionManager newSessionManager) { debug("attempting to restore old session..."); WebSocketClientSessionManager oldSessionManager; synchronized (clientSessionManagerMap) { oldSessionManager = this.clientSessionManagerMap.get(oldSessionId); } if (oldSessionManager == null) { // cannot restore old context debug("restore failed"); return false; } else { //TODO: replace the sessionManager's socket with the new one oldSessionManager.setSocket(newSessionManager.getSocketKey()); synchronized (clientSessionManagerMap) { /* remove pointers to new session manager since we're using the old one */ this.clientSessionManagerMap.remove(newSessionManager.getSessionId()); this.clientSessionHandleMap.remove(newSessionManager.getSessionId()); } this.debug("old session restored"); return true; } } /** * send update message to a websocket client, uid is 0 for update message * @param update * update message * @param conn * websocket client */ public void sendUpdateMessage(UpdateMessage update, WebSocket conn) { createPacketFromMessageAndSend(0, update, conn); } /** * serialize the oodss message, attach the uid, convert to bytes, and send to the websocket client * the bytes array has its first 8 bits dedicated to uid, and the rest bits to the body of the * serialized oodss message * the message is encoded in UTF-8 * * @param uid * uid of the request message * @param message * oodss message * @param conn * websocket client */ private void createPacketFromMessageAndSend(long uid, ServiceMessage message, WebSocket conn) { StringBuilder messageStringBuilder = new StringBuilder(); try { SimplTypesScope.serialize(message, messageStringBuilder, StringFormat.XML); String messageString = messageStringBuilder.toString(); byte[] uidBytes = longToBytes(uid); byte[] messageBytes = messageString.getBytes("UTF-8"); byte[] outMessage = new byte[uidBytes.length + messageBytes.length]; System.arraycopy(uidBytes, 0, outMessage, 0, uidBytes.length); System.arraycopy(messageBytes, 0, outMessage, uidBytes.length, messageBytes.length); conn.send(outMessage); } catch (SIMPLTranslationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NotYetConnectedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * process the received raw message from websocket client * the raw message is encoded in UTF-8, the first 8 bits are converted to uid, the rest are oodss message. * * @param conn * @param messageBytes */ public void processReceivedMessage(WebSocket conn, ByteBuffer messageBytes) { // obtain uid byte[] messageByteArray = messageBytes.array(); byte[] uidArray = Arrays.copyOfRange(messageByteArray, 0, 8); byte[] messageArray = Arrays.copyOfRange(messageByteArray, 8, messageByteArray.length); long uid = bytesToLong(uidArray); currentMessage = new String(); try { currentMessage = new String(messageArray, "UTF-8"); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } debug("Got the message: " + currentMessage + " from uid: " + uid); try { processRead(conn, uid, currentMessage); } catch (BadClientException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * find webSocket client's corresponding session manager, and let the session manager process the oodss message. * If the session manager does not exist, a new session manager will be generated. * * @param conn * @param uid * @param message * @throws BadClientException */ protected void processRead(WebSocket conn, long uid, String message) throws BadClientException { if (currentMessage.length() > 0) { if (conn instanceof WebSocketImpl) { SelectionKey key = ((WebSocketImpl)conn).key; if (key != null) { String sessionToken = ((WebSocketImpl)conn).socket.toString(); if (!sessionToken.isEmpty() && sessionToken != null) { synchronized (clientSessionManagerMap) { WebSocketClientSessionManager cm = clientSessionManagerMap.get(sessionToken); if (cm == null) { debug("server creating context manager for " + sessionToken); //TODO: cm = generateContextManager(sessionToken, key, conn, translationScope, applicationObjectScope); clientSessionManagerMap.put(sessionToken, cm); clientSessionHandleMap.put(sessionToken, cm.getHandle()); } ResponseMessage responseMessage = cm.processString(message, uid); createPacketFromMessageAndSend(uid, responseMessage, conn); } } } } synchronized (this) { this.notify(); } } } /** * Hook method to allow changing the ContextManager to enable specific extra functionality. * * @param sessionToken * @param translationScope * @param applicationObjectScope * @return */ protected WebSocketClientSessionManager generateContextManager( String sessionToken, SelectionKey key, WebSocket conn, SimplTypesScope translationScope, Scope applicationObjectScope) { return new WebSocketClientSessionManager(sessionToken, key, conn, translationScope, applicationObjectScope, this); } /** * convenience method to convert long to bytes * * @param uid * @return */ private byte[] longToBytes(long uid) { return ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(uid).array(); } /** * convenience method to convert bytes to long * * @param bytes * @return */ private long bytesToLong(byte[] bytes) { ByteBuffer bb = ByteBuffer.wrap(bytes); return bb.order(ByteOrder.LITTLE_ENDIAN).getLong(); } public void shutdownClient(WebSocket conn) { String sessionToken = ((WebSocketImpl)conn).socket.toString(); if (sessionToken!=null) clientSessionManagerMap.get(sessionToken).shutdown(); } }