org.atmosphere.nettosphere.BridgeRuntime.java Source code

Java tutorial

Introduction

Here is the source code for org.atmosphere.nettosphere.BridgeRuntime.java

Source

/*
 * Copyright 2016 Async-IO.org
 *
 * 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.nettosphere;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.ReferenceCountUtil;
import org.atmosphere.container.NettyCometSupport;
import org.atmosphere.cpr.Action;
import org.atmosphere.cpr.ApplicationConfig;
import org.atmosphere.cpr.AsynchronousProcessor;
import org.atmosphere.cpr.AtmosphereConfig;
import org.atmosphere.cpr.AtmosphereFramework;
import org.atmosphere.cpr.AtmosphereHandler;
import org.atmosphere.cpr.AtmosphereInterceptor;
import org.atmosphere.cpr.AtmosphereMappingException;
import org.atmosphere.cpr.AtmosphereRequest;
import org.atmosphere.cpr.AtmosphereRequestImpl;
import org.atmosphere.cpr.AtmosphereResource;
import org.atmosphere.cpr.AtmosphereResourceImpl;
import org.atmosphere.cpr.AtmosphereResponse;
import org.atmosphere.cpr.AtmosphereResponseImpl;
import org.atmosphere.cpr.Broadcaster;
import org.atmosphere.cpr.FrameworkConfig;
import org.atmosphere.cpr.HeaderConfig;
import org.atmosphere.cpr.WebSocketProcessorFactory;
import org.atmosphere.util.FakeHttpSession;
import org.atmosphere.websocket.WebSocket;
import org.atmosphere.websocket.WebSocketEventListener;
import org.atmosphere.websocket.WebSocketHandler;
import org.atmosphere.websocket.WebSocketPingPongListener;
import org.atmosphere.websocket.WebSocketProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.ClosedChannelException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static io.netty.channel.ChannelHandler.Sharable;
import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import static io.netty.handler.codec.http.HttpHeaders.setContentLength;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static org.atmosphere.cpr.AtmosphereFramework.REFLECTOR_ATMOSPHEREHANDLER;
import static org.atmosphere.cpr.HeaderConfig.SSE_TRANSPORT;
import static org.atmosphere.cpr.HeaderConfig.X_ATMOSPHERE_TRANSPORT;
import static org.atmosphere.websocket.WebSocketEventListener.WebSocketEvent.TYPE.HANDSHAKE;

/**
 * Bridge the Atmosphere Framework with Netty.
 *
 * @author Jeanfrancois Arcand
 */
@Sharable
public class BridgeRuntime extends HttpStaticFileServerHandler {

    public static boolean NETTY_41_PLUS;

    static {
        try {
            Class.forName("io.netty.channel.ChannelId");
            NETTY_41_PLUS = true;
        } catch (ClassNotFoundException e) {
            NETTY_41_PLUS = false;
        }
    }

    private final static String KEEP_ALIVE = BridgeRuntime.class.getName() + "_keep-alive";
    private static final Logger logger = LoggerFactory.getLogger(BridgeRuntime.class);

    private final AtmosphereFramework framework;
    private final Config config;
    private final ScheduledExecutorService suspendTimer;
    private final ConcurrentHashMap<String, HttpSession> sessions = new ConcurrentHashMap<>();
    private final AtomicBoolean isShutdown = new AtomicBoolean();
    private final WebSocketProcessor webSocketProcessor;
    private final ChannelGroup httpChannels = new DefaultChannelGroup("http", ImmediateEventExecutor.INSTANCE);
    private final ChannelGroup websocketChannels = new DefaultChannelGroup("ws", ImmediateEventExecutor.INSTANCE);
    private final AsynchronousProcessor asynchronousProcessor;
    private final int maxWebSocketFrameSize;

    private final AtmosphereRequest proxiedRequest;
    private final AtmosphereResponse proxiedResponse;
    private final AtmosphereResource proxiedResource;

    private int webSocketTimeout = -1;
    private final byte[] EMPTY = new byte[0];

    public BridgeRuntime(final Config config) {
        super(config.path());
        this.config = config;
        framework = new AtmosphereFramework();

        if (config.broadcaster() != null) {
            framework.setDefaultBroadcasterClassName(config.broadcaster().getName());
        }

        framework.setAtmosphereDotXmlPath(config.configFile());

        try {
            if (config.broadcasterFactory() != null) {
                framework.setBroadcasterFactory(config.broadcasterFactory());
            }
        } catch (Throwable t) {
            logger.trace("", t);
        }

        if (config.broadcasterCache() != null) {
            try {
                framework.setBroadcasterCacheClassName(config.broadcasterCache().getName());
            } catch (Throwable t) {
                logger.trace("", t);
            }
        }

        Map<String, AtmosphereHandler> handlersMap = config.handlersMap();
        for (Map.Entry<String, AtmosphereHandler> e : handlersMap.entrySet()) {
            framework.addAtmosphereHandler(e.getKey(), e.getValue());
        }

        final Map<String, WebSocketHandler> webSocketHandlerMap = config.webSocketHandlersMap();

        if (handlersMap.size() == 0 && !webSocketHandlerMap.isEmpty()) {
            framework.addAtmosphereHandler(Broadcaster.ROOT_MASTER, REFLECTOR_ATMOSPHEREHANDLER);
        }

        framework.getAtmosphereConfig().startupHook(new AtmosphereConfig.StartupHook() {
            @Override
            public void started(AtmosphereFramework framework) {
                for (Map.Entry<String, WebSocketHandler> e : webSocketHandlerMap.entrySet()) {
                    framework.addWebSocketHandler(e.getKey(), e.getValue());
                }
            }
        });

        if (config.webSocketProtocol() != null) {
            framework.setWebSocketProtocolClassName(config.webSocketProtocol().getName());
        }

        for (AtmosphereInterceptor i : config.interceptors()) {
            framework.interceptor(i);
        }

        if (!config.scanPackages().isEmpty()) {
            for (Class<?> s : config.scanPackages()) {
                framework.addAnnotationPackage(s);
            }
        }

        final Context context = new Context.Builder().attributes(config.servletContextAttributes())
                .contextPath(config.mappingPath()).basePath(config.path()).build();
        ServletContext ctx = (ServletContext) Proxy.newProxyInstance(BridgeRuntime.class.getClassLoader(),
                new Class[] { ServletContext.class }, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        try {
                            Method stub = Context.class.getMethod(method.getName(), method.getParameterTypes());
                            if (stub != null) {
                                return stub.invoke(context, args);
                            } else {
                                logger.trace("Method {} not supported", method.getName());
                                return null;
                            }
                        } catch (NoSuchMethodException ex) {
                            logger.trace("Method {} not supported", method.getName());
                            return null;
                        }
                    }
                });

        try {
            framework.externalizeDestroy(true).init(new NettyServletConfig(config.initParams(), ctx));
        } catch (ServletException e) {
            throw new RuntimeException(e);
        }

        int max = framework.getAtmosphereConfig().getInitParameter(ApplicationConfig.WEBSOCKET_IDLETIME, -1);
        if (max != -1) {
            webSocketTimeout = max;
        }

        framework.setAsyncSupport(new NettyCometSupport(framework.getAtmosphereConfig()) {
            @Override
            public Action suspended(AtmosphereRequest request, AtmosphereResponse response)
                    throws IOException, ServletException {
                Action a = super.suspended(request, response);
                if (framework.getAtmosphereConfig().isSupportSession()) {
                    AtmosphereResource r = request.resource();
                    HttpSession s = request.getSession(true);
                    if (s != null) {
                        sessions.put(r.uuid(), request.getSession(true));
                    }
                }
                return a;
            }

            @Override
            public String toString() {
                return "NettoSphereAsyncSupport";
            }
        });

        suspendTimer = new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors());
        webSocketProcessor = WebSocketProcessorFactory.getDefault().getWebSocketProcessor(framework);

        for (String s : config.excludedInterceptors()) {
            framework.excludeInterceptor(s);
        }
        asynchronousProcessor = AsynchronousProcessor.class.cast(framework.getAsyncSupport());
        maxWebSocketFrameSize = config.maxWebSocketFrameSize();

        if (config.noInternalAlloc()) {
            proxiedRequest = new AtmosphereRequestImpl.Builder().build();
            proxiedResponse = new AtmosphereResponseImpl.Builder().build();
            proxiedResource = new AtmosphereResourceImpl();
        } else {
            proxiedRequest = null;
            proxiedResponse = null;
            proxiedResource = null;
        }
    }

    public AtmosphereFramework framework() {
        return framework;
    }

    @Override
    public void channelRead(final ChannelHandlerContext ctx, final Object messageEvent)
            throws URISyntaxException, IOException {
        try {
            handleMessageEvent(ctx, messageEvent);
        } finally {
            if (messageEvent instanceof ReferenceCounted) {
                ReferenceCounted refMsg = (ReferenceCounted) messageEvent;
                if (refMsg.refCnt() > 0)
                    refMsg.release();
            }
        }
    }

    private void handleMessageEvent(final ChannelHandlerContext ctx, final Object messageEvent)
            throws URISyntaxException, IOException {
        Object msg = messageEvent;

        if (isShutdown.get()) {
            ctx.channel().close().addListener(ChannelFutureListener.CLOSE);
            return;
        }

        if (msg instanceof HttpRequest) {
            HttpRequest r = HttpRequest.class.cast(msg);
            // Netty fail to decode headers separated by a ','
            List<String> c = r.headers().getAll("Connection");
            String u = r.headers().get("Upgrade");
            boolean webSocket = false;
            if (u != null && u.equalsIgnoreCase("websocket")) {
                webSocket = true;
            }

            for (String connection : c) {
                if (connection != null && connection.toLowerCase().equalsIgnoreCase("upgrade")) {
                    webSocket = true;
                }
            }

            logger.trace("Handling request {}", r);
            if (webSocket) {
                handleWebSocketHandshake(ctx, messageEvent);
            } else {
                handleHttp(ctx, messageEvent);
            }
        } else if (msg instanceof WebSocketFrame) {
            handleWebSocketFrame(ctx, messageEvent);
        } else if (msg instanceof HttpChunkedInput) {
            handleHttp(ctx, messageEvent);
        }
    }

    private void handleWebSocketHandshake(final ChannelHandlerContext ctx, Object messageEvent)
            throws IOException, URISyntaxException {
        final HttpRequest request = (HttpRequest) messageEvent;

        // Allow only GET methods.
        if (request.getMethod() != GET) {
            sendHttpResponse(ctx, request, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN));
            return;
        }

        ctx.pipeline().addBefore(BridgeRuntime.class.getName(), "encoder", new HttpResponseEncoder());
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                getWebSocketLocation(request), config.subProtocols(), false, maxWebSocketFrameSize);

        WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(request);

        if (handshaker == null) {
            wsFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            final NettyWebSocket webSocket = new NettyWebSocket(ctx.channel(), framework.getAtmosphereConfig(),
                    config.noInternalAlloc(), config.binaryWrite());
            final AtmosphereRequest atmosphereRequest = createAtmosphereRequest(ctx, request, EMPTY);

            if (!webSocketProcessor.handshake(atmosphereRequest)) {
                sendError(ctx, HttpResponseStatus.BAD_REQUEST, null);
                return;
            }

            webSocketProcessor.notifyListener(webSocket,
                    new WebSocketEventListener.WebSocketEvent("", HANDSHAKE, webSocket));

            handshaker.handshake(ctx.channel(), (FullHttpRequest) request).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (!future.isSuccess()) {
                        future.channel().close();
                    } else {
                        websocketChannels.add(ctx.channel());

                        ctx.channel().attr(ATTACHMENT).set(webSocket);
                        if (config.noInternalAlloc()) {
                            webSocket.resource(proxiedResource);
                        }

                        AtmosphereResponse response = config.noInternalAlloc() ? proxiedResponse
                                : AtmosphereResponseImpl.newInstance(framework.getAtmosphereConfig(),
                                        atmosphereRequest, webSocket);
                        webSocketProcessor.open(webSocket, atmosphereRequest, response);

                        if (webSocketTimeout > 0) {
                            webSocket.closeFuture(suspendTimer.scheduleAtFixedRate(new Runnable() {
                                @Override
                                public void run() {
                                    if (webSocket.lastWriteTimeStampInMilliseconds() != 0
                                            && (System.currentTimeMillis() - webSocket
                                                    .lastWriteTimeStampInMilliseconds() > webSocketTimeout)) {
                                        logger.debug("Timing out {}", webSocket);
                                        webSocket.close();
                                    }
                                }
                            }, webSocketTimeout, webSocketTimeout, TimeUnit.MILLISECONDS));
                        }
                    }
                }
            });
        }
    }

    private void handleWebSocketFrame(final ChannelHandlerContext ctx, final Object messageEvent)
            throws URISyntaxException, IOException {
        WebSocketFrame frame = (WebSocketFrame) messageEvent;

        logger.trace("Received frame {}", frame.getClass().getName());

        // Check for closing frame
        WebSocket attachment = WebSocket.class.cast(ctx.channel().attr(ATTACHMENT).get());
        ByteBuf binaryData = frame.content();
        byte[] body = null;
        if (binaryData.isReadable()) {
            body = new byte[binaryData.readableBytes()];
            binaryData.readBytes(body);
        }
        if (frame instanceof CloseWebSocketFrame) {
            ctx.channel().write(frame).addListener(ChannelFutureListener.CLOSE);
        } else if (frame instanceof PingWebSocketFrame) {
            if (WebSocketPingPongListener.class.isAssignableFrom(webSocketProcessor.getClass())) {
                WebSocketPingPongListener.class.cast(webSocketProcessor).onPing(attachment, body, 0, body.length);
            } else {
                ctx.channel().writeAndFlush(new PongWebSocketFrame(binaryData));
            }
        } else if (frame instanceof BinaryWebSocketFrame
                || (frame instanceof TextWebSocketFrame && config.textFrameAsBinary())) {
            webSocketProcessor.invokeWebSocketProtocol(attachment, body, 0, body.length);
        } else if (frame instanceof TextWebSocketFrame) {
            webSocketProcessor.invokeWebSocketProtocol(attachment, ((TextWebSocketFrame) frame).text());
        } else if (frame instanceof PongWebSocketFrame) {

            if (WebSocketPingPongListener.class.isAssignableFrom(webSocketProcessor.getClass())) {
                WebSocketPingPongListener.class.cast(webSocketProcessor).onPong(attachment, body, 0, body.length);
            }

            if (config.enablePong()) {
                ctx.channel().writeAndFlush(new PingWebSocketFrame(frame.content()));
            } else {
                logger.trace("Received Pong Frame on Channel {}", ctx.channel());
            }
        } else {
            logger.warn("{} frame types not supported", frame.getClass());
            ctx.channel().close();
        }
    }

    private AtmosphereRequest createAtmosphereRequest(final ChannelHandlerContext ctx, final HttpRequest request,
            byte[] body) throws URISyntaxException, UnsupportedEncodingException, MalformedURLException {
        final String base = getBaseUri(request);
        final URI requestUri = new URI(base.substring(0, base.length() - 1) + request.getUri());
        final String ct = HttpHeaders.getHeader(request, "Content-Type", "text/plain");
        final long cl = HttpHeaders.getContentLength(request, 0);
        String method = request.getMethod().name();

        String queryString = requestUri.getQuery();
        Map<String, String[]> qs = new HashMap<String, String[]>();
        if (queryString != null) {
            parseQueryString(qs, queryString);
        }

        if (ct.equalsIgnoreCase("application/x-www-form-urlencoded")) {
            if (FullHttpRequest.class.isAssignableFrom(request.getClass())) {
                parseQueryString(qs, new String(body));
            }
        }

        String u = requestUri.toURL().toString();
        int last = u.indexOf("?") == -1 ? u.length() : u.indexOf("?");
        String url = u.substring(0, last);
        int l;

        if (url.contains(config.mappingPath())) {
            l = requestUri.getAuthority().length() + requestUri.getScheme().length() + 3
                    + config.mappingPath().length();
        } else {
            l = requestUri.getAuthority().length() + requestUri.getScheme().length() + 3;
        }

        HttpSession session = null;
        if (framework.getAtmosphereConfig().isSupportSession()) {
            String[] transport = qs.get(HeaderConfig.X_ATMOSPHERE_TRANSPORT);
            if (transport != null && transport.length > 0) {
                String[] uuid = qs.get(HeaderConfig.X_ATMOSPHERE_TRACKING_ID);
                if (uuid != null && uuid.length > 0) {
                    // TODO: Session is only supported until an unsubscribe is received.
                    if (transport[0].equalsIgnoreCase(HeaderConfig.DISCONNECT_TRANSPORT_MESSAGE)) {
                        sessions.remove(uuid[0]);
                    } else {
                        session = sessions.get(uuid[0]);

                        if (session == null) {
                            session = new FakeHttpSession("-1", null, System.currentTimeMillis(), -1);
                        }
                    }
                }
            }
        }

        final Map<String, Object> attributes = new HashMap<String, Object>();
        AtmosphereRequestImpl.Builder requestBuilder = new AtmosphereRequestImpl.Builder();
        requestBuilder.requestURI(url.substring(l)).requestURL(url).pathInfo(url.substring(l))
                .headers(getHeaders(request)).method(method).contentType(ct).contentLength(cl)
                // We need to read attribute after doComet
                .destroyable(false).attributes(attributes).servletPath(config.mappingPath()).session(session)
                .cookies(getCookies(request)).queryStrings(qs)
                .remoteInetSocketAddress(new Callable<InetSocketAddress>() {
                    @Override
                    public InetSocketAddress call() throws Exception {
                        return (InetSocketAddress) ctx.channel().remoteAddress();
                    }
                }).localInetSocketAddress(new Callable<InetSocketAddress>() {

                    @Override
                    public InetSocketAddress call() throws Exception {
                        return (InetSocketAddress) ctx.channel().localAddress();
                    }
                });

        if (body.length > 0) {
            requestBuilder.body(body);
        }

        return requestBuilder.build();

    }

    private void handleHttp(final ChannelHandlerContext ctx, final Object messageEvent)
            throws URISyntaxException, IOException {

        boolean skipClose = false;
        AtmosphereResponse response = null;
        AtmosphereRequest request = null;
        Action a = null;
        boolean resumeOnBroadcast = false;
        boolean keptOpen = false;
        ChannelWriter asyncWriter = null;
        String method = "GET";
        boolean writeHeader = false;
        boolean forceSuspend = false;
        boolean aggregateBodyInMemory = config.aggregateRequestBodyInMemory();

        try {
            if (messageEvent instanceof HttpRequest) {
                final HttpRequest hrequest = (HttpRequest) messageEvent;

                byte[] body = EMPTY;
                if (FullHttpRequest.class.isAssignableFrom(messageEvent.getClass())) {
                    ByteBuf b = FullHttpRequest.class.cast(messageEvent).content();
                    if (b.isReadable()) {
                        body = new byte[b.readableBytes()];
                        b.readBytes(body);
                    }
                }

                // First let's try to see if it's a static resources
                if (!hrequest.getUri().contains(HeaderConfig.X_ATMOSPHERE)) {
                    try {
                        hrequest.headers().add(STATIC_MAPPING, "true");
                        super.channelRead(ctx, messageEvent);

                        if (HttpHeaders.getHeader(hrequest, SERVICED) != null) {
                            return;
                        }
                    } catch (Exception e) {
                        logger.debug("Unexpected State", e);
                    } finally {
                        hrequest.headers().set(STATIC_MAPPING, "false");
                    }
                }

                boolean ka = HttpHeaders.isKeepAlive(hrequest);
                asyncWriter = config.supportChunking() ? new ChunkedWriter(ctx.channel(), true, ka)
                        : new StreamWriter(ctx.channel(), true, ka);

                method = hrequest.getMethod().name();

                request = createAtmosphereRequest(ctx, hrequest, body);
                request.setAttribute(KEEP_ALIVE, new Boolean(ka));

                // Hacky. Is the POST doesn't contains a body, we must not close the connection yet.
                AtmosphereRequestImpl.Body b = request.body();
                if (!aggregateBodyInMemory && !hrequest.getMethod().equals(GET) && !b.isEmpty()
                        && (b.hasString() && b.asString().isEmpty()) || (b.hasBytes() && b.asBytes().length == 0)) {
                    forceSuspend = true;
                }
            } else {
                request = State.class.cast(ctx.attr(ATTACHMENT).get()).request;
                boolean isLast = HttpChunkedInput.class.cast(messageEvent).isEndOfInput();
                Boolean ka = (Boolean) request.getAttribute(KEEP_ALIVE);

                asyncWriter = config.supportChunking() ? new ChunkedWriter(ctx.channel(), isLast, ka)
                        : new StreamWriter(ctx.channel(), isLast, ka);
                method = request.getMethod();
                ByteBuf internalBuffer = HttpChunkedInput.class.cast(messageEvent).readChunk(ctx).content();

                if (!aggregateBodyInMemory && internalBuffer.hasArray()) {
                    request.body(internalBuffer.array());
                } else {
                    logger.trace("Unable to read in memory the request's bytes. Using stream");
                    request.body(new ByteBufInputStream(internalBuffer));
                }

                if (!isLast) {
                    forceSuspend = true;
                }
            }

            response = new AtmosphereResponseImpl.Builder().asyncIOWriter(asyncWriter).writeHeader(writeHeader)
                    .destroyable(false).header("Connection", "Keep-Alive").header("Server", "Nettosphere/3.0")
                    .request(request).build();

            if (config.supportChunking()) {
                response.setHeader("Transfer-Encoding", "chunked");
            }

            a = framework.doCometSupport(request, response);
            if (forceSuspend) {
                a.type(Action.TYPE.SUSPEND);
                // leave the stream open
                keptOpen = true;
            }

            String transport = (String) request.getAttribute(FrameworkConfig.TRANSPORT_IN_USE);
            if (transport == null) {
                transport = request.getHeader(X_ATMOSPHERE_TRANSPORT);
            }

            if (a.type() == Action.TYPE.SUSPEND) {
                if (transport != null && (transport.equalsIgnoreCase(HeaderConfig.STREAMING_TRANSPORT)
                        || transport.equalsIgnoreCase(SSE_TRANSPORT))) {
                    keptOpen = true;
                } else if (transport != null && (transport.equalsIgnoreCase(HeaderConfig.LONG_POLLING_TRANSPORT)
                        || transport.equalsIgnoreCase(HeaderConfig.JSONP_TRANSPORT))) {
                    resumeOnBroadcast = true;
                }
            }

            final Action action = (Action) request.getAttribute(NettyCometSupport.SUSPEND);
            final State state = new State(request, action == null ? Action.CONTINUE : action);

            ctx.attr(ATTACHMENT).set(state);

            if (action != null && action.type() == Action.TYPE.SUSPEND) {
                if (action.timeout() != -1) {
                    final AtomicReference<ChannelWriter> w = new AtomicReference<ChannelWriter>(asyncWriter);
                    final AtomicReference<Future<?>> f = new AtomicReference<Future<?>>();
                    f.set(suspendTimer.scheduleAtFixedRate(new Runnable() {
                        @Override
                        public void run() {
                            if (!w.get().isClosed()
                                    && (System.currentTimeMillis() - w.get().lastTick()) > action.timeout()) {
                                AtmosphereResourceImpl impl = state.resource();
                                if (impl != null) {
                                    asynchronousProcessor.endRequest(impl, false);
                                    f.get().cancel(true);
                                }
                            }
                        }
                    }, action.timeout(), action.timeout(), TimeUnit.MILLISECONDS));
                }
            } else if (action != null && action.type() == Action.TYPE.RESUME) {
                resumeOnBroadcast = false;
            }
        } catch (AtmosphereMappingException ex) {
            if (method.equalsIgnoreCase("GET")) {
                logger.trace("Unable to map the request {}, trying static file", messageEvent);
            }
        } catch (Throwable e) {
            logger.error("Unable to process request", e);
            throw new IOException(e);
        } finally {
            try {
                if (asyncWriter != null && !resumeOnBroadcast && !keptOpen) {
                    if (!skipClose && response != null) {
                        asyncWriter.close(response);
                    } else {
                        httpChannels.add(ctx.channel());
                    }
                }
            } finally {
                if (request != null && a != null && a.type() != Action.TYPE.SUSPEND) {
                    request.destroy();
                    response.destroy();
                    framework.notify(Action.TYPE.DESTROYED, request, response);
                }
            }
        }
    }

    @Override
    public void sendError(ChannelHandlerContext ctx, HttpResponseStatus status, FullHttpRequest e) {
        // For websocket, we can't send an error
        logger.trace("Error {} for {}", status, e);
        if (websocketChannels.contains(ctx.channel())) {
            logger.debug("Error {} for {}", status, e);
            ctx.channel().close().addListener(ChannelFutureListener.CLOSE);
        } else if (e != null) {
            if (HttpHeaders.getHeader(e, STATIC_MAPPING, "false").equalsIgnoreCase("false")) {
                super.sendError(ctx, status, e);
            }
        } else {
            super.sendError(ctx, status, e);
        }
    }

    public void destroy() {
        isShutdown.set(true);
        if (framework != null)
            framework.destroy();

        httpChannels.close();
        websocketChannels.write(new CloseWebSocketFrame());
        suspendTimer.shutdown();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        Object o = ctx.attr(ATTACHMENT).get();

        if (o == null)
            return;

        if (WebSocket.class.isAssignableFrom(o.getClass())) {
            NettyWebSocket webSocket = NettyWebSocket.class.cast(o);
            logger.trace("Closing {}", webSocket.uuid());

            try {
                if (webSocket.closeFuture() != null) {
                    webSocket.closeFuture().cancel(true);
                }

                webSocketProcessor.close(webSocket, 1005);
            } catch (Exception ex) {
                logger.error("{}", webSocket, ex);
            }
        } else if (State.class.isAssignableFrom(o.getClass())) {
            logger.trace("State {}", o);
            State s = State.class.cast(o);
            if (s.action.type() == Action.TYPE.SUSPEND) {
                asynchronousProcessor.endRequest(s.resource(), true);
            }
        } else {
            logger.error("Invalid state {} and Channel {}", o, ctx.channel());
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) {
        // Ignore Disconnect exception.
        if (e.getCause() != null && (e.getCause().getClass().equals(ClosedChannelException.class)
                || e.getCause().getClass().equals(IOException.class))) {
            logger.trace("Exception", e.getCause());
        } else if (e.getCause() != null && e.getCause().getClass().equals(TooLongFrameException.class)) {
            logger.error(
                    "TooLongFrameException. The request will be closed, make sure you increase the Config.maxChunkContentLength() to a higher value.",
                    e.getCause());
            super.exceptionCaught(ctx, e);
        } else {
            logger.debug("Exception", e.getCause());
            super.exceptionCaught(ctx, e);
        }
    }

    private Map<String, String> getHeaders(final HttpRequest request) {
        final Map<String, String> headers = new HashMap<String, String>();

        for (String name : request.headers().names()) {
            // TODO: Add support for multi header
            headers.put(name, HttpHeaders.getHeader(request, name));
        }

        return headers;
    }

    private String getBaseUri(final HttpRequest request) {
        return "http://" + HttpHeaders.getHeader(request, HttpHeaders.Names.HOST, "127.0.0.1") + "/";

    }

    private void parseQueryString(Map<String, String[]> qs, String queryString) {
        if (queryString != null) {
            String[] s = queryString.split("&");
            for (String a : s) {
                String[] q = a.split("=");
                String[] z = new String[] { q.length > 1 ? q[1] : "" };
                qs.put(q[0], z);
            }
        }
    }

    private Set<javax.servlet.http.Cookie> getCookies(final HttpRequest request) {
        Set<javax.servlet.http.Cookie> result = new HashSet<javax.servlet.http.Cookie>();
        String cookieHeader = request.headers().get("Cookie");
        if (cookieHeader != null) {
            Set<io.netty.handler.codec.http.cookie.Cookie> cookies = ServerCookieDecoder.LAX.decode(cookieHeader);
            for (io.netty.handler.codec.http.cookie.Cookie cookie : cookies) {
                javax.servlet.http.Cookie c = new javax.servlet.http.Cookie(cookie.name(), cookie.value());

                if (cookie.domain() != null) {
                    c.setDomain(cookie.domain());
                }

                c.setHttpOnly(cookie.isHttpOnly());
                c.setMaxAge((int) cookie.maxAge());
                if (cookie.path() != null) {
                    c.setPath(cookie.path());
                }

                c.setSecure(cookie.isSecure());
                result.add(c);

            }
        }
        return result;
    }

    Config config() {
        return config;
    }

    private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {
        // Generate an error page if response status code is not OK (200).
        if (res.getStatus().code() != 200) {
            FullHttpResponse response = (FullHttpResponse) res;
            response.content().writeBytes(Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8));
            setContentLength(res, response.content().readableBytes());
        }

        // Send the response and close the connection if necessary.
        ChannelFuture f = ctx.channel().write(res);
        if (!isKeepAlive(req) || res.getStatus().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }

    public final static class State {
        final AtmosphereRequest request;
        final Action action;

        public State(AtmosphereRequest request, Action action) {
            this.request = request;
            this.action = action;
        }

        public AtmosphereResourceImpl resource() {
            return AtmosphereResourceImpl.class.cast(request.resource());
        }
    }

    private String getWebSocketLocation(HttpRequest req) {
        return "ws://" + req.headers().get(HttpHeaders.Names.HOST) + req.getUri();
    }

    private final static class NettyServletConfig implements ServletConfig {

        private final Map<String, String> initParams;
        private final ServletContext ctx;

        public NettyServletConfig(Map<String, String> initParams, ServletContext ctx) {
            this.initParams = initParams;
            this.ctx = ctx;
        }

        @Override
        public String getServletName() {
            return "Netty";
        }

        @Override
        public ServletContext getServletContext() {
            return ctx;
        }

        @Override
        public String getInitParameter(String name) {
            return initParams.get(name);
        }

        @Override
        public Enumeration getInitParameterNames() {
            return Collections.enumeration(initParams.keySet());
        }
    }

    public ChannelGroup httpChannels() {
        return httpChannels;
    }

    public ChannelGroup websocketChannels() {
        return websocketChannels;
    }

}