co.paralleluniverse.comsat.webactors.netty.WebActorHandler.java Source code

Java tutorial

Introduction

Here is the source code for co.paralleluniverse.comsat.webactors.netty.WebActorHandler.java

Source

/*
/*
 * COMSAT
 * Copyright (c) 2015-2016, Parallel Universe Software Co. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 3.0
 * as published by the Free Software Foundation.
 */
package co.paralleluniverse.comsat.webactors.netty;

import co.paralleluniverse.actors.*;
import co.paralleluniverse.common.util.Pair;
import co.paralleluniverse.common.util.SystemProperties;
import co.paralleluniverse.comsat.webactors.*;
import co.paralleluniverse.comsat.webactors.Cookie;
import co.paralleluniverse.comsat.webactors.HttpRequest;
import co.paralleluniverse.comsat.webactors.HttpResponse;
import co.paralleluniverse.fibers.SuspendExecution;
import co.paralleluniverse.fibers.Suspendable;
import co.paralleluniverse.strands.Timeout;
import co.paralleluniverse.strands.channels.SendPort;
import co.paralleluniverse.strands.concurrent.CountDownLatch;
import co.paralleluniverse.strands.concurrent.ReentrantLock;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;

import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpHeaders.Names.*;

import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author circlespainter
 */
public class WebActorHandler extends SimpleChannelInboundHandler<Object> {
    protected static final ScheduledExecutorService ts = Executors
            .newScheduledThreadPool(Runtime.getRuntime().availableProcessors());

    // @FunctionalInterface
    public interface WebActorContextProvider {
        Context get(FullHttpRequest req);
    }

    public interface Context {
        String getId();

        ReentrantLock getLock();

        boolean isValid() throws SuspendExecution, InterruptedException;

        void invalidate() throws SuspendExecution, InterruptedException;

        boolean renew();

        void restart(FullHttpRequest r);

        ActorRef<? extends WebMessage> getWebActor();

        boolean handlesWithHttp(String uri);

        boolean handlesWithWebSocket(String uri);

        enum WatchPolicy {
            DONT_WATCH, DIE, DIE_IF_EXCEPTION_ELSE_RESTART, RESTART
        }

        WatchPolicy watch();

        Map<String, Object> getAttachments();
    }

    public static abstract class DefaultContextImpl implements Context {
        private final static String durationProp = System
                .getProperty(DefaultContextImpl.class.getName() + ".durationMillis");
        private final static long DURATION = durationProp != null ? Long.parseLong(durationProp) : 60_000L;
        private final ReentrantLock lock = new ReentrantLock();
        @SuppressWarnings("unused")
        private final long created;
        private final Map<String, Object> attachments = new HashMap<>();

        protected long renewed;
        private Long validityMS;

        private boolean valid = true;

        public DefaultContextImpl() {
            renewed = created = new Date().getTime();
        }

        @Override
        public void invalidate() throws SuspendExecution, InterruptedException {
            final HttpActorAdapter actor = (HttpActorAdapter) attachments.get(ACTOR_KEY);
            if (actor != null)
                actor.handleDie(null);
            attachments.clear();
            valid = false;
        }

        @Override
        public final boolean isValid() throws SuspendExecution, InterruptedException {
            final boolean ret = valid && (new Date().getTime() - renewed) <= getValidityMS();
            if (!ret)
                invalidate();
            return ret;
        }

        @Override
        public final boolean renew() {
            if (!valid)
                return false;

            renewed = new Date().getTime();
            return true;
        }

        @Override
        public final Map<String, Object> getAttachments() {
            return attachments;
        }

        @Override
        public final ReentrantLock getLock() {
            return lock;
        }

        public void setValidityMS(long validityMS) {
            this.validityMS = validityMS;
        }

        public long getValidityMS() {
            return validityMS != null ? validityMS : DURATION;
        }
    }

    public WebActorHandler(WebActorContextProvider contextProvider) {
        this(contextProvider, null);
    }

    public WebActorHandler(WebActorContextProvider contextProvider, String httpResponseEncoderName) {
        this.contextProvider = contextProvider;
        this.httpResponseEncoderName = httpResponseEncoderName;
    }

    @Override
    public final void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        if (ctx.channel().isOpen())
            ctx.close();
        log.error("Exception caught", cause);
    }

    @Override
    protected final void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(ctx, (WebSocketFrame) msg);
        } else {
            throw new AssertionError("Unexpected message " + msg);
        }
    }

    protected static boolean sessionsEnabled() {
        return "always".equals(trackSession) || "sse".equals(trackSession);
    }

    protected final static String SESSION_COOKIE_KEY = "JSESSIONID";
    protected final static String TRACK_SESSION_PROP = HttpChannelAdapter.class.getName() + ".trackSession";
    protected final static String trackSession = System.getProperty(TRACK_SESSION_PROP, "sse");

    protected final static String OMIT_DATE_HEADER_PROP = HttpChannelAdapter.class.getName() + ".omitDateHeader";
    protected final static Boolean omitDateHeader = SystemProperties.isEmptyOrTrue(OMIT_DATE_HEADER_PROP);

    protected final static ConcurrentHashMap<String, Context> sessions = new ConcurrentHashMap<>();
    protected final static ReentrantLock sessionsLock = new ReentrantLock();

    protected WebActorContextProvider contextProvider;
    protected String httpResponseEncoderName;

    private static final String ACTOR_KEY = "co.paralleluniverse.comsat.webactors.sessionActor";

    private static final WeakHashMap<Class<?>, List<Pair<String, String>>> classToUrlPatterns = new WeakHashMap<>();
    private static final InternalLogger log = InternalLoggerFactory.getInstance(AutoWebActorHandler.class);

    private WebSocketServerHandshaker handshaker;
    private WebSocketActorAdapter webSocketActor;

    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // Check for closing frame
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }

        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
            return;
        }

        if (frame instanceof ContinuationWebSocketFrame)
            return;

        if (frame instanceof TextWebSocketFrame)
            webSocketActor.onMessage(((TextWebSocketFrame) frame).text());
        else
            webSocketActor.onMessage(frame.content().nioBuffer());
    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req)
            throws SuspendExecution, InterruptedException {
        // Handle a bad request.
        if (!req.getDecoderResult().isSuccess()) {
            sendHttpError(ctx, req, new DefaultFullHttpResponse(req.getProtocolVersion(), BAD_REQUEST));
            return;
        }

        final String uri = req.getUri();

        final Context actorCtx = contextProvider.get(req);
        assert actorCtx != null;
        final String sessionId = actorCtx.getId();
        assert sessionId != null;

        final ReentrantLock lock = actorCtx.getLock();
        assert lock != null;

        lock.lock();

        try {
            final ActorRef<? extends WebMessage> userActorRef = actorCtx.getWebActor();
            ActorImpl internalActor = (ActorImpl) actorCtx.getAttachments().get(ACTOR_KEY);

            if (userActorRef != null) {
                if (actorCtx.handlesWithWebSocket(uri)) {
                    if (internalActor == null || !(internalActor instanceof WebSocketActorAdapter)) {
                        //noinspection unchecked
                        webSocketActor = new WebSocketActorAdapter(ctx,
                                (ActorRef<? super WebMessage>) userActorRef);
                        addActorToContextAndUnlock(actorCtx, webSocketActor, lock);
                    }
                    // Handshake
                    final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(uri,
                            null, true);
                    handshaker = wsFactory.newHandshaker(req);
                    if (handshaker == null) {
                        WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
                    } else {
                        @SuppressWarnings("unchecked")
                        final ActorRef<WebMessage> userActorRef0 = (ActorRef<WebMessage>) webSocketActor.userActor;
                        handshaker.handshake(ctx.channel(), req)
                                .addListener(new GenericFutureListener<ChannelFuture>() {
                                    @Override
                                    @Suspendable
                                    public void operationComplete(ChannelFuture future) throws Exception {
                                        userActorRef0.send(
                                                new WebSocketOpened(WebActorHandler.this.webSocketActor.ref()));
                                    }
                                });
                    }
                    return;
                } else if (actorCtx.handlesWithHttp(uri)) {
                    if (internalActor == null || !(internalActor instanceof HttpActorAdapter)) {
                        //noinspection unchecked
                        internalActor = new HttpActorAdapter((ActorRef<HttpRequest>) userActorRef, actorCtx,
                                httpResponseEncoderName);
                        addActorToContextAndUnlock(actorCtx, internalActor, lock);
                    }
                    //noinspection unchecked
                    ((HttpActorAdapter) internalActor)
                            .handleRequest(new HttpRequestWrapper(internalActor.ref(), ctx, req, sessionId));
                    return;
                }
            }
        } finally {
            if (lock.isHeldByCurrentStrand() && lock.isLocked())
                lock.unlock();
        }

        sendHttpError(ctx, req, new DefaultFullHttpResponse(req.getProtocolVersion(), NOT_FOUND));
    }

    static void addActorToContextAndUnlock(Context actorContext, ActorImpl actor, ReentrantLock lock) {
        try {
            actorContext.getAttachments().put(ACTOR_KEY, actor);
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentStrand())
                lock.unlock();
        }
    }

    private static final class WebSocketActorAdapter extends FakeActor<WebDataMessage> {
        ActorRef<? super WebMessage> userActor;

        private ChannelHandlerContext ctx;

        public WebSocketActorAdapter(ChannelHandlerContext ctx, ActorRef<? super WebMessage> userActor) {
            super(userActor.getName(), new WebSocketChannelAdapter(ctx));
            ((WebSocketChannelAdapter) (SendPort) getMailbox()).actor = this;
            this.ctx = ctx;
            this.userActor = userActor;
            watch(userActor);
        }

        @Override
        public final void interrupt() {
            die(new InterruptedException());
        }

        @Override
        public final String toString() {
            return "WebSocketActorAdapter{" + "userActor=" + userActor + '}';
        }

        private void onMessage(final ByteBuffer message) {
            try {
                userActor.send(new WebDataMessage(ref(), message));
            } catch (SuspendExecution ex) {
                throw new AssertionError(ex);
            }
        }

        private void onMessage(final String message) {
            try {
                userActor.send(new WebDataMessage(ref(), message));
            } catch (SuspendExecution ex) {
                throw new AssertionError(ex);
            }
        }

        @Override
        protected final WebDataMessage handleLifecycleMessage(LifecycleMessage m) {
            if (m instanceof ExitMessage) {
                ExitMessage em = (ExitMessage) m;
                if (em.getActor() != null && em.getActor().equals(userActor))
                    die(em.getCause());
            }
            return null;
        }

        @Override
        protected final void throwIn(RuntimeException e) {
            die(e);
        }

        @Override
        protected final void die(Throwable cause) {
            super.die(cause);
            if (ctx.channel().isOpen())
                ctx.close();

            // Ensure to release server references
            userActor = null;
            ctx = null;
        }
    }

    private static final class WebSocketChannelAdapter implements SendPort<WebDataMessage> {
        private final ChannelHandlerContext ctx;

        WebSocketActorAdapter actor;

        public WebSocketChannelAdapter(ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }

        @Override
        public final void send(WebDataMessage message) throws SuspendExecution, InterruptedException {
            trySend(message);
        }

        @Override
        public final boolean send(WebDataMessage message, long timeout, TimeUnit unit)
                throws SuspendExecution, InterruptedException {
            return trySend(message);
        }

        @Override
        public final boolean send(WebDataMessage message, Timeout timeout)
                throws SuspendExecution, InterruptedException {
            return send(message, timeout.nanosLeft(), TimeUnit.NANOSECONDS);
        }

        @Override
        public final boolean trySend(WebDataMessage message) {
            if (!message.isBinary())
                ctx.writeAndFlush(new TextWebSocketFrame(message.getStringBody()));
            else
                ctx.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message.getByteBufferBody())));
            return true;
        }

        @Override
        public final void close() {
            if (ctx.channel().isOpen())
                ctx.close();
            actor.die(null);
        }

        @Override
        public final void close(Throwable t) {
            actor.die(t);
            close();
        }
    }

    private static final class HttpActorAdapter extends FakeActor<HttpResponse> {
        private final static String replyTimeoutProp = System
                .getProperty(HttpActorAdapter.class.getName() + ".replyTimeout");
        private static final long REPLY_TIMEOUT = replyTimeoutProp != null ? Long.parseLong(replyTimeoutProp)
                : 120_000L;

        private final AtomicReference<CountDownLatch> gate = new AtomicReference<>();

        private final String httpResponseEncoderName;

        private volatile ActorRef<? super HttpRequest> userActor;
        private volatile Context context;

        private volatile ChannelHandlerContext ctx;
        private volatile FullHttpRequest req;
        private volatile boolean needsRestart;

        private volatile Object watchToken;
        private volatile boolean dead;

        private volatile ScheduledFuture<?> cancelTask;

        HttpActorAdapter(ActorRef<? super HttpRequest> userActor, Context actorContext,
                String httpResponseEncoderName) {
            super("HttpActorAdapter", new HttpChannelAdapter());

            ((HttpChannelAdapter) (SendPort) getMailbox()).actor = this;
            if (actorContext.watch() != Context.WatchPolicy.DONT_WATCH) {
                if (userActor != null)
                    watchToken = watch(userActor);
            }

            this.userActor = userActor;
            this.context = actorContext;
            this.httpResponseEncoderName = httpResponseEncoderName;
        }

        @Override
        public final String toString() {
            return "HttpActorAdapter{" + userActor + "}";
        }

        @Override
        @Suspendable
        protected final HttpResponse handleLifecycleMessage(LifecycleMessage m) {
            handleLifecycle(m);
            return null;
        }

        @Override
        @Suspendable
        protected final void die(Throwable cause) {
            handleDie(cause);
        }

        @Override
        @Suspendable
        protected final void throwIn(RuntimeException e) {
            die(e);
        }

        @Override
        @Suspendable
        protected final void interrupt() {
            die(new InterruptedException());
        }

        final boolean handleLifecycle(LifecycleMessage l) {
            if (l instanceof ExitMessage) {
                final ExitMessage em = (ExitMessage) l;
                if (em.getActor() != null && em.getActor().equals(userActor)) {
                    possiblyReplyDeadAndUnblock(em.getCause());

                    final Context.WatchPolicy wp = context.watch();
                    //noinspection ThrowableResultOfMethodCallIgnored
                    if (wp == Context.WatchPolicy.RESTART
                            || wp == Context.WatchPolicy.DIE_IF_EXCEPTION_ELSE_RESTART && em.getCause() == null) {
                        needsRestart = true;
                    } else {
                        die(em.getCause());
                        return true;
                    }
                }
            }
            return false;
        }

        final void handleRequest(HttpRequestWrapper s) throws SuspendExecution, InterruptedException {
            blockSessionRequests();

            ctx = s.ctx;
            req = s.req;
            if (needsRestart) {
                context.restart(req);
                context.getLock().lock();
                addActorToContextAndUnlock(context, HttpActorAdapter.this, context.getLock());
                needsRestart = false;
            }
            userActor.send(s);
        }

        @Suspendable
        final void handleReply(HttpResponse message) throws SuspendExecution, InterruptedException {
            try {
                final HttpRequestWrapper nettyRequest = (HttpRequestWrapper) message.getRequest();
                final FullHttpRequest req = nettyRequest.req;
                final ChannelHandlerContext ctx = nettyRequest.ctx;
                final String sessionId = nettyRequest.getSessionId();

                final HttpResponseStatus status = HttpResponseStatus.valueOf(message.getStatus());

                if (message.getStatus() >= 400 && message.getStatus() < 600) {
                    sendHttpError(ctx, req, new DefaultFullHttpResponse(req.getProtocolVersion(), status));
                    return;
                }

                if (message.getRedirectPath() != null) {
                    sendHttpRedirect(ctx, req, message.getRedirectPath());
                    return;
                }

                final FullHttpResponse res;
                if (message.getStringBody() != null)
                    res = new DefaultFullHttpResponse(req.getProtocolVersion(), status,
                            Unpooled.wrappedBuffer(message.getStringBody().getBytes()));
                else if (message.getByteBufferBody() != null)
                    res = new DefaultFullHttpResponse(req.getProtocolVersion(), status,
                            Unpooled.wrappedBuffer(message.getByteBufferBody()));
                else
                    res = new DefaultFullHttpResponse(req.getProtocolVersion(), status);

                if (message.getCookies() != null) {
                    final ServerCookieEncoder enc = ServerCookieEncoder.STRICT;
                    for (final Cookie c : message.getCookies())
                        HttpHeaders.setHeader(res, COOKIE, enc.encode(getNettyCookie(c)));
                }
                if (message.getHeaders() != null) {
                    for (final Map.Entry<String, String> h : message.getHeaders().entries())
                        HttpHeaders.setHeader(res, h.getKey(), h.getValue());
                }

                if (message.getContentType() != null) {
                    String ct = message.getContentType();
                    if (message.getCharacterEncoding() != null)
                        ct = ct + "; charset=" + message.getCharacterEncoding().name();
                    HttpHeaders.setHeader(res, CONTENT_TYPE, ct);
                }

                final boolean sseStarted = message.shouldStartActor();
                if (trackSession(sseStarted)) {
                    res.headers().add(SET_COOKIE, ServerCookieEncoder.STRICT.encode(SESSION_COOKIE_KEY, sessionId));
                    startSession(sessionId, context);
                }
                if (!sseStarted) {
                    final String stringBody = message.getStringBody();
                    long contentLength = 0L;
                    if (stringBody != null)
                        contentLength = stringBody.getBytes().length;
                    else {
                        final ByteBuffer byteBufferBody = message.getByteBufferBody();
                        if (byteBufferBody != null)
                            contentLength = byteBufferBody.remaining();
                    }
                    res.headers().add(CONTENT_LENGTH, contentLength);
                }

                final HttpStreamActorAdapter httpStreamActorAdapter;
                if (sseStarted)
                    // This will copy the request content, which must still be referenceable, doing before the request handler
                    // unallocates it (unfortunately it is explicitly reference-counted in Netty)
                    httpStreamActorAdapter = new HttpStreamActorAdapter(ctx, req);
                else
                    httpStreamActorAdapter = null;

                sendHttpResponse(ctx, req, res);

                if (sseStarted) {
                    if (httpResponseEncoderName != null) {
                        ctx.pipeline().remove(httpResponseEncoderName);
                    } else {
                        final ChannelPipeline pl = ctx.pipeline();
                        final List<String> handlerKeysToBeRemoved = new ArrayList<>();
                        for (final Map.Entry<String, ChannelHandler> e : pl) {
                            if (e.getValue() instanceof HttpResponseEncoder)
                                handlerKeysToBeRemoved.add(e.getKey());
                        }
                        for (final String k : handlerKeysToBeRemoved)
                            pl.remove(k);
                    }

                    try {
                        message.getFrom().send(new HttpStreamOpened(httpStreamActorAdapter.ref(), message));
                    } catch (final SuspendExecution e) {
                        throw new AssertionError(e);
                    }
                }
            } finally {
                unblockSessionRequests();
            }
        }

        @Suspendable
        final void handleDie(Throwable cause) {
            possiblyReplyDeadAndUnblock(cause);

            if (dead)
                return;
            dead = true;
            HttpActorAdapter.super.die(cause);
            try {
                context.invalidate();
            } catch (final Exception ignored) {
            }

            // Ensure to release references
            if (userActor != null && watchToken != null)
                unwatch(userActor, watchToken);

            userActor = null;
            watchToken = null;
            context = null;
            ctx = null;
            req = null;
        }

        private void possiblyReplyDeadAndUnblock(Throwable cause) {
            if (isRequestInProgress()) { // Req replied
                try {
                    if (cause != null) {
                        sendHttpError(ctx, req,
                                new DefaultFullHttpResponse(req.getProtocolVersion(), INTERNAL_SERVER_ERROR,
                                        Unpooled.wrappedBuffer(
                                                ("Actor is dead because of " + cause.getMessage()).getBytes())));
                    } else {
                        sendHttpError(ctx, req,
                                new DefaultFullHttpResponse(req.getProtocolVersion(), INTERNAL_SERVER_ERROR,
                                        Unpooled.wrappedBuffer(("Actor has terminated.").getBytes())));
                    }
                } finally {
                    unblockSessionRequests();
                }
            }
        }

        @Suspendable
        private void blockSessionRequests() throws InterruptedException {
            while (!gate.compareAndSet(null, new CountDownLatch(1))) {
                final CountDownLatch l = gate.get();
                if (l != null)
                    l.await();
            }
            final ChannelHandlerContext ctx1 = ctx;
            final FullHttpRequest req1 = req;
            cancelTask = ts.schedule(new Runnable() {
                @Override
                public void run() {
                    try {
                        sendHttpError(ctx1, req1,
                                new DefaultFullHttpResponse(req1.getProtocolVersion(), INTERNAL_SERVER_ERROR,
                                        Unpooled.wrappedBuffer(
                                                ("Timeout while waiting for user actor to reply.").getBytes())));
                    } finally {
                        unblockSessionRequests();
                    }
                }
            }, REPLY_TIMEOUT, TimeUnit.MILLISECONDS);
        }

        @Suspendable
        private void unblockSessionRequests() {
            if (cancelTask != null)
                cancelTask.cancel(true);
            final CountDownLatch l = gate.getAndSet(null);
            if (l != null)
                l.countDown();
        }

        private boolean isRequestInProgress() {
            return gate.get() != null;
        }
    }

    private static final class HttpChannelAdapter implements SendPort<HttpResponse> {
        HttpActorAdapter actor;

        @Override
        @Suspendable
        public final void send(HttpResponse message) throws SuspendExecution, InterruptedException {
            trySend(message);
        }

        @Override
        @Suspendable
        public final boolean send(HttpResponse message, long timeout, TimeUnit unit)
                throws SuspendExecution, InterruptedException {
            send(message);
            return true;
        }

        @Override
        @Suspendable
        public final boolean send(HttpResponse message, Timeout timeout)
                throws SuspendExecution, InterruptedException {
            return send(message, timeout.nanosLeft(), TimeUnit.NANOSECONDS);
        }

        @Override
        @Suspendable
        public final boolean trySend(HttpResponse r) {
            try {
                actor.handleReply(r);
            } catch (final SuspendExecution e) {
                throw new AssertionError(e);
            } catch (final InterruptedException e) {
                throw new RuntimeException(e);
            }
            return true;
        }

        @Override
        @Suspendable
        public final void close() {
            actor.die(null);
        }

        @Override
        @Suspendable
        public final void close(Throwable t) {
            log.error("Exception while closing HTTP adapter", t);
            actor.die(t);
        }
    }

    protected static boolean trackSession(boolean sseStarted) {
        return trackSession != null && ("always".equals(trackSession) || sseStarted && "sse".equals(trackSession));
    }

    protected static boolean handlesWithHttp(String uri, Class<?> actorClass) {
        return match(uri, actorClass).equals("websocket");
    }

    protected static boolean handlesWithWebSocket(String uri, Class<?> actorClass) {
        return match(uri, actorClass).equals("ws");
    }

    private static final class HttpStreamActorAdapter extends FakeActor<WebDataMessage> {
        private volatile boolean dead;

        public HttpStreamActorAdapter(final ChannelHandlerContext ctx, final FullHttpRequest req) {
            super(req.toString(), new HttpStreamChannelAdapter(ctx, req));
            ((HttpStreamChannelAdapter) (SendPort) getMailbox()).actor = this;
        }

        @Override
        protected WebDataMessage handleLifecycleMessage(LifecycleMessage m) {
            if (m instanceof ShutdownMessage) {
                die(null);
            }
            return null;
        }

        @Override
        protected void throwIn(RuntimeException e) {
            die(e);
        }

        @Override
        public void interrupt() {
            die(new InterruptedException());
        }

        @Override
        protected void die(Throwable cause) {
            if (dead)
                return;
            this.dead = true;
            mailbox().close();
            super.die(cause);
        }

        @Override
        public String toString() {
            return "HttpStreamActorAdapter{request + " + getName() + "}";
        }
    }

    private static final class HttpStreamChannelAdapter implements SendPort<WebDataMessage> {
        private final Charset encoding;
        private final ChannelHandlerContext ctx;

        HttpStreamActorAdapter actor;

        public HttpStreamChannelAdapter(ChannelHandlerContext ctx, FullHttpRequest req) {
            this.ctx = ctx;
            this.encoding = HttpRequestWrapper.extractCharacterEncodingOrDefault(req.headers());
        }

        @Override
        public final void send(WebDataMessage message) throws SuspendExecution, InterruptedException {
            trySend(message);
        }

        @Override
        public final boolean send(WebDataMessage message, long timeout, TimeUnit unit)
                throws SuspendExecution, InterruptedException {
            send(message);
            return true;
        }

        @Override
        public final boolean send(WebDataMessage message, Timeout timeout)
                throws SuspendExecution, InterruptedException {
            return send(message, timeout.nanosLeft(), TimeUnit.NANOSECONDS);
        }

        @Override
        public final boolean trySend(WebDataMessage res) {
            final ByteBuf buf;
            final String stringBody = res.getStringBody();
            if (stringBody != null) {
                byte[] bs = stringBody.getBytes(encoding);
                buf = Unpooled.wrappedBuffer(bs);
            } else {
                buf = Unpooled.wrappedBuffer(res.getByteBufferBody());
            }
            ctx.writeAndFlush(buf);
            return true;
        }

        @Override
        public final void close() {
            if (ctx.channel().isOpen())
                ctx.close();
            actor.die(null);
        }

        @Override
        public final void close(Throwable t) {
            actor.die(t);
            close();
        }
    }

    static void startSession(String sessionId, Context actorContext) throws SuspendExecution, InterruptedException {
        sessionsLock.lock();
        try {
            cleanSessions();
            sessions.put(sessionId, actorContext);
        } finally {
            sessionsLock.unlock();
        }
    }

    private static void cleanSessions() throws SuspendExecution, InterruptedException {
        // TODO less often
        final Collection<String> toBeRemoved = new ArrayList<>(1024);
        for (final Map.Entry<String, Context> e : sessions.entrySet()) {
            if (!e.getValue().isValid())
                toBeRemoved.add(e.getKey());
        }
        for (final String s : toBeRemoved)
            sessions.remove(s);
    }

    static io.netty.handler.codec.http.cookie.Cookie getNettyCookie(Cookie c) {
        io.netty.handler.codec.http.cookie.Cookie ret = new io.netty.handler.codec.http.cookie.DefaultCookie(
                c.getName(), c.getValue());
        ret.setDomain(c.getDomain());
        ret.setHttpOnly(c.isHttpOnly());
        ret.setMaxAge(c.getMaxAge());
        ret.setPath(c.getPath());
        ret.setSecure(c.isSecure());
        return ret;
    }

    static void sendHttpRedirect(ChannelHandlerContext ctx, FullHttpRequest req, String newUri) {
        final FullHttpResponse res = new DefaultFullHttpResponse(req.getProtocolVersion(), FOUND);
        HttpHeaders.setHeader(res, LOCATION, newUri);
        writeHttpResponse(ctx, req, res, true);
    }

    static void sendHttpError(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
        sendHttpResponse(ctx, req, res, true);
    }

    static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
        sendHttpResponse(ctx, req, res, false);
    }

    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res,
            boolean close) {
        writeHttpResponse(ctx, req, res, close);
    }

    private static void writeHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res,
            boolean close) {
        if (!omitDateHeader && !res.headers().contains(DefaultHttpHeaders.Names.DATE))
            DefaultHttpHeaders.addDateHeader(res, DefaultHttpHeaders.Names.DATE, new Date());

        // Reply the response and close the connection if necessary.
        if (!HttpHeaders.isKeepAlive(req) || close) {
            res.headers().set(CONNECTION, HttpHeaders.Values.CLOSE);
            ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE);
        } else {
            res.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
            write(ctx, res);
        }
    }

    private static ChannelFuture write(ChannelHandlerContext ctx, Object res) {
        return ctx.writeAndFlush(res);
        // : ctx.write(res);
    }

    private static String match(String uri, Class<?> actorClass) {
        if (uri != null && actorClass != null) {
            for (final Pair<String, String> e : lookupOrInsert(actorClass)) {
                if (servletMatch(e.getFirst(), uri))
                    return e.getSecond();
            }
        }
        return "";
    }

    private static List<Pair<String, String>> lookupOrInsert(Class<?> actorClass) {
        if (actorClass != null) {
            final List<Pair<String, String>> lookup = classToUrlPatterns.get(actorClass);
            if (lookup != null)
                return lookup;
            return insert(actorClass);
        }
        return null;
    }

    private static List<Pair<String, String>> insert(Class<?> actorClass) {
        if (actorClass != null) {
            final WebActor wa = actorClass.getAnnotation(WebActor.class);
            final List<Pair<String, String>> ret = new ArrayList<>(4);
            for (String httpP : wa.httpUrlPatterns())
                addPattern(ret, httpP, "websocket");
            for (String wsP : wa.webSocketUrlPatterns())
                addPattern(ret, wsP, "ws");
            classToUrlPatterns.put(actorClass, ret);
            return ret;
        }
        return null;
    }

    private static void addPattern(List<Pair<String, String>> ret, String p, String type) {
        if (p != null) {
            @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
            final Pair<String, String> entry = new Pair<>(p, type);
            if (p.endsWith("*") || p.startsWith("*.") || p.equals("/")) // Wildcard -> end
                ret.add(entry);
            else // Exact -> beginning
                ret.add(0, entry);
        }
    }

    private static boolean servletMatch(String pattern, String uri) {
        // As per servlet spec
        if (pattern != null && uri != null) {
            if (pattern.startsWith("/") && pattern.endsWith("*"))
                return uri.startsWith(pattern.substring(0, pattern.length() - 1));
            if (pattern.startsWith("*."))
                return uri.endsWith(pattern.substring(2));
            if (pattern.isEmpty())
                return uri.equals("/");
            return pattern.equals("/") || pattern.equals(uri);
        }
        return false;
    }
}