package jj.http.server.websocket; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; import org.mozilla.javascript.Callable; import jj.execution.ExecutionLifecycleAware; import jj.script.FunctionContext; import jj.util.DateFormatHelper; @Singleton public class WebSocketConnection implements FunctionContext, ExecutionLifecycleAware { // start off with room for three functions private final HashMap<String, Callable> functions = new HashMap<>(4); private final ChannelHandlerContext ctx; private final WebSocketConnectionHost webSocketConnectionHost; private final HashMap<String, Object> clientStorage = new HashMap<>(4); private final List<WebSocketMessage> messages = new ArrayList<>(4); private final String description; // accessed from many threads private volatile long lastActivity = System.currentTimeMillis(); @Inject WebSocketConnection( final ChannelHandlerContext ctx, final WebSocketConnectionHost webSocketConnectionHost ) { this.ctx = ctx; this.webSocketConnectionHost = webSocketConnectionHost; description = String.format( "WebSocket connection to %s started at %s", ctx.channel().remoteAddress(), DateFormatHelper.nowInBasicFormat() ); } @Override public Callable getFunction(String name) { return functions.get(name); } @Override public void addFunction(String name, Callable function) { functions.put(name, function); } @Override public boolean removeFunction(String name) { return functions.remove(name) != null; } @Override public boolean removeFunction(String name, Callable function) { return (functions.get(name) == function) && (functions.remove(name) == function); } void markActivity() { lastActivity = System.currentTimeMillis(); } long lastActivity() { return lastActivity; } public String baseName() { return webSocketConnectionHost().name(); } public WebSocketConnectionHost webSocketConnectionHost() { return webSocketConnectionHost; } public Map<String, Object> clientStorage() { return clientStorage; } public WebSocketConnection send(WebSocketMessage message) { messages.add(message); return this; } @Override public void enteredScope() { // nothing to do } @Override public void exitedScope() { if (!messages.isEmpty()) { String message = serialize(); ctx.writeAndFlush(new TextWebSocketFrame(message)); } } private String serialize() { StringBuilder output = new StringBuilder(); if (!messages.isEmpty()) { output.append("["); for (WebSocketMessage message : messages) { output.append(message.stringify()).append(','); } output.setCharAt(output.length() - 1, ']'); messages.clear(); } return output.toString(); } @Override public String toString() { return description; } /** * */ public void close() { ctx.writeAndFlush(new CloseWebSocketFrame(1000, null)).addListener(ChannelFutureListener.CLOSE); } }