/*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.http.nio.protocol;

import java.io.IOException;

import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseFactory;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.ProtocolException;
import org.apache.http.ProtocolVersion;
import org.apache.http.UnsupportedHttpVersionException;
import org.apache.http.annotation.ThreadingBehavior;
import org.apache.http.annotation.Contract;
import org.apache.http.nio.ContentDecoder;
import org.apache.http.nio.ContentEncoder;
import org.apache.http.nio.IOControl;
import org.apache.http.nio.NHttpServerConnection;
import org.apache.http.nio.NHttpServiceHandler;
import org.apache.http.nio.entity.ConsumingNHttpEntity;
import org.apache.http.nio.entity.NByteArrayEntity;
import org.apache.http.nio.entity.NHttpEntityWrapper;
import org.apache.http.nio.entity.ProducingNHttpEntity;
import org.apache.http.nio.util.ByteBufferAllocator;
import org.apache.http.nio.util.HeapByteBufferAllocator;
import org.apache.http.params.DefaultedHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpExpectationVerifier;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.util.Args;
import org.apache.http.util.Asserts;
import org.apache.http.util.EncodingUtils;

/**
 * Fully asynchronous HTTP server side protocol handler implementation that
 * implements the essential requirements of the HTTP protocol for the server
 * side message processing as described by RFC 2616. It is capable of processing
 * HTTP requests with nearly constant memory footprint. Only HTTP message heads
 * are stored in memory, while content of message bodies is streamed directly
 * from the entity to the underlying channel (and vice versa)
 * {@link ConsumingNHttpEntity} and {@link ProducingNHttpEntity} interfaces.
 * <p>
 * When using this class, it is important to ensure that entities supplied for
 * writing implement {@link ProducingNHttpEntity}. Doing so will allow the
 * entity to be written out asynchronously. If entities supplied for writing do
 * not implement {@link ProducingNHttpEntity}, a delegate is added that buffers
 * the entire contents in memory. Additionally, the buffering might take place
 * in the I/O thread, which could cause I/O to block temporarily. For best
 * results, ensure that all entities set on {@link HttpResponse}s from
 * {@link NHttpRequestHandler}s implement {@link ProducingNHttpEntity}.
 * <p>
 * If incoming requests enclose a content entity, {@link NHttpRequestHandler}s
 * are expected to return a {@link ConsumingNHttpEntity} for reading the
 * content. After the entity is finished reading the data,
 * {@link NHttpRequestHandler#handle(HttpRequest, HttpResponse, NHttpResponseTrigger, HttpContext)}
 * is called to generate a response.
 * <p>
 * Individual {@link NHttpRequestHandler}s do not have to submit a response
 * immediately. They can defer transmission of the HTTP response back to the
 * client without blocking the I/O thread and to delegate the processing the
 * HTTP request to a worker thread. The worker thread in its turn can use an
 * instance of {@link NHttpResponseTrigger} passed as a parameter to submit
 * a response as at a later point of time once the response becomes available.
 *
 * @since 4.0
 *
 * @deprecated (4.2) use {@link HttpAsyncService}
 */
@Deprecated
@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
public class AsyncNHttpServiceHandler extends NHttpHandlerBase
                                      implements NHttpServiceHandler {

    protected final HttpResponseFactory responseFactory;

    protected NHttpRequestHandlerResolver handlerResolver;
    protected HttpExpectationVerifier expectationVerifier;

    public AsyncNHttpServiceHandler(
            final HttpProcessor httpProcessor,
            final HttpResponseFactory responseFactory,
            final ConnectionReuseStrategy connStrategy,
            final ByteBufferAllocator allocator,
            final HttpParams params) {
        super(httpProcessor, connStrategy, allocator, params);
        Args.notNull(responseFactory, "Response factory");
        this.responseFactory = responseFactory;
    }

    public AsyncNHttpServiceHandler(
            final HttpProcessor httpProcessor,
            final HttpResponseFactory responseFactory,
            final ConnectionReuseStrategy connStrategy,
            final HttpParams params) {
        this(httpProcessor, responseFactory, connStrategy, HeapByteBufferAllocator.INSTANCE, params);
    }

    public void setExpectationVerifier(final HttpExpectationVerifier expectationVerifier) {
        this.expectationVerifier = expectationVerifier;
    }

    public void setHandlerResolver(final NHttpRequestHandlerResolver handlerResolver) {
        this.handlerResolver = handlerResolver;
    }

    @Override
    public void connected(final NHttpServerConnection conn) {
        final HttpContext context = conn.getContext();

        final ServerConnState connState = new ServerConnState();
        context.setAttribute(CONN_STATE, connState);
        context.setAttribute(ExecutionContext.HTTP_CONNECTION, conn);

        if (this.eventListener != null) {
            this.eventListener.connectionOpen(conn);
        }
    }

    @Override
    public void requestReceived(final NHttpServerConnection conn) {
        final HttpContext context = conn.getContext();

        final ServerConnState connState = (ServerConnState) context.getAttribute(CONN_STATE);

        final HttpRequest request = conn.getHttpRequest();
        request.setParams(new DefaultedHttpParams(request.getParams(), this.params));

        connState.setRequest(request);

        final NHttpRequestHandler requestHandler = getRequestHandler(request);
        connState.setRequestHandler(requestHandler);

        ProtocolVersion ver = request.getRequestLine().getProtocolVersion();
        if (!ver.lessEquals(HttpVersion.HTTP_1_1)) {
            // Downgrade protocol version if greater than HTTP/1.1
            ver = HttpVersion.HTTP_1_1;
        }

        HttpResponse response;

        try {

            if (request instanceof HttpEntityEnclosingRequest) {
                final HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
                if (entityRequest.expectContinue()) {
                    response = this.responseFactory.newHttpResponse(
                            ver, HttpStatus.SC_CONTINUE, context);
                    response.setParams(
                            new DefaultedHttpParams(response.getParams(), this.params));

                    if (this.expectationVerifier != null) {
                        try {
                            this.expectationVerifier.verify(request, response, context);
                        } catch (final HttpException ex) {
                            response = this.responseFactory.newHttpResponse(
                                    HttpVersion.HTTP_1_0,
                                    HttpStatus.SC_INTERNAL_SERVER_ERROR,
                                    context);
                            response.setParams(
                                    new DefaultedHttpParams(response.getParams(), this.params));
                            handleException(ex, response);
                        }
                    }

                    if (response.getStatusLine().getStatusCode() < 200) {
                        // Send 1xx response indicating the server expections
                        // have been met
                        conn.submitResponse(response);
                    } else {
                        conn.resetInput();
                        sendResponse(conn, request, response);
                    }
                }
                // Request content is expected.
                ConsumingNHttpEntity consumingEntity = null;

                // Lookup request handler for this request
                if (requestHandler != null) {
                    consumingEntity = requestHandler.entityRequest(entityRequest, context);
                }
                if (consumingEntity == null) {
                    consumingEntity = new NullNHttpEntity(entityRequest.getEntity());
                }
                entityRequest.setEntity(consumingEntity);
                connState.setConsumingEntity(consumingEntity);

            } else {
                // No request content is expected.
                // Process request right away
                conn.suspendInput();
                processRequest(conn, request);
            }

        } catch (final IOException ex) {
            shutdownConnection(conn, ex);
            if (this.eventListener != null) {
                this.eventListener.fatalIOException(ex, conn);
            }
        } catch (final HttpException ex) {
            closeConnection(conn, ex);
            if (this.eventListener != null) {
                this.eventListener.fatalProtocolException(ex, conn);
            }
        }

    }

    @Override
    public void closed(final NHttpServerConnection conn) {
        final HttpContext context = conn.getContext();

        final ServerConnState connState = (ServerConnState) context.getAttribute(CONN_STATE);
        try {
            connState.reset();
        } catch (final IOException ex) {
            if (this.eventListener != null) {
                this.eventListener.fatalIOException(ex, conn);
            }
        }
        if (this.eventListener != null) {
            this.eventListener.connectionClosed(conn);
        }
    }

    @Override
    public void exception(final NHttpServerConnection conn, final HttpException httpex) {
        if (conn.isResponseSubmitted()) {
            // There is not much that we can do if a response head
            // has already been submitted
            closeConnection(conn, httpex);
            if (eventListener != null) {
                eventListener.fatalProtocolException(httpex, conn);
            }
            return;
        }

        final HttpContext context = conn.getContext();
        try {
            final HttpResponse response = this.responseFactory.newHttpResponse(
                    HttpVersion.HTTP_1_0, HttpStatus.SC_INTERNAL_SERVER_ERROR, context);
            response.setParams(
                    new DefaultedHttpParams(response.getParams(), this.params));
            handleException(httpex, response);
            response.setEntity(null);
            sendResponse(conn, null, response);

        } catch (final IOException ex) {
            shutdownConnection(conn, ex);
            if (this.eventListener != null) {
                this.eventListener.fatalIOException(ex, conn);
            }
        } catch (final HttpException ex) {
            closeConnection(conn, ex);
            if (this.eventListener != null) {
                this.eventListener.fatalProtocolException(ex, conn);
            }
        }
    }

    @Override
    public void exception(final NHttpServerConnection conn, final IOException ex) {
        shutdownConnection(conn, ex);

        if (this.eventListener != null) {
            this.eventListener.fatalIOException(ex, conn);
        }
    }

    @Override
    public void timeout(final NHttpServerConnection conn) {
        handleTimeout(conn);
    }

    @Override
    public void inputReady(final NHttpServerConnection conn, final ContentDecoder decoder) {
        final HttpContext context = conn.getContext();
        final ServerConnState connState = (ServerConnState) context.getAttribute(CONN_STATE);

        final HttpRequest request = connState.getRequest();
        final ConsumingNHttpEntity consumingEntity = connState.getConsumingEntity();

        try {

            consumingEntity.consumeContent(decoder, conn);
            if (decoder.isCompleted()) {
                conn.suspendInput();
                processRequest(conn, request);
            }

        } catch (final IOException ex) {
            shutdownConnection(conn, ex);
            if (this.eventListener != null) {
                this.eventListener.fatalIOException(ex, conn);
            }
        } catch (final HttpException ex) {
            closeConnection(conn, ex);
            if (this.eventListener != null) {
                this.eventListener.fatalProtocolException(ex, conn);
            }
        }
    }

    @Override
    public void responseReady(final NHttpServerConnection conn) {
        final HttpContext context = conn.getContext();
        final ServerConnState connState = (ServerConnState) context.getAttribute(CONN_STATE);

        if (connState.isHandled()) {
            return;
        }

        final HttpRequest request = connState.getRequest();

        try {

            final IOException ioex = connState.getIOException();
            if (ioex != null) {
                throw ioex;
            }

            final HttpException httpex = connState.getHttpException();
            if (httpex != null) {
                final HttpResponse response = this.responseFactory.newHttpResponse(HttpVersion.HTTP_1_0,
                        HttpStatus.SC_INTERNAL_SERVER_ERROR, context);
                response.setParams(
                        new DefaultedHttpParams(response.getParams(), this.params));
                handleException(httpex, response);
                connState.setResponse(response);
            }

            final HttpResponse response = connState.getResponse();
            if (response != null) {
                connState.setHandled(true);
                sendResponse(conn, request, response);
            }

        } catch (final IOException ex) {
            shutdownConnection(conn, ex);
            if (this.eventListener != null) {
                this.eventListener.fatalIOException(ex, conn);
            }
        } catch (final HttpException ex) {
            closeConnection(conn, ex);
            if (this.eventListener != null) {
                this.eventListener.fatalProtocolException(ex, conn);
            }
        }
    }

    @Override
    public void outputReady(final NHttpServerConnection conn, final ContentEncoder encoder) {
        final HttpContext context = conn.getContext();
        final ServerConnState connState = (ServerConnState) context.getAttribute(CONN_STATE);

        final HttpResponse response = conn.getHttpResponse();

        try {
            final ProducingNHttpEntity entity = connState.getProducingEntity();
            entity.produceContent(encoder, conn);

            if (encoder.isCompleted()) {
                connState.finishOutput();
                if (!this.connStrategy.keepAlive(response, context)) {
                    conn.close();
                } else {
                    // Ready to process new request
                    connState.reset();
                    conn.requestInput();
                }
                responseComplete(response, context);
            }

        } catch (final IOException ex) {
            shutdownConnection(conn, ex);
            if (this.eventListener != null) {
                this.eventListener.fatalIOException(ex, conn);
            }
        }
    }

    private void handleException(final HttpException ex, final HttpResponse response) {
        int code = HttpStatus.SC_INTERNAL_SERVER_ERROR;
        if (ex instanceof MethodNotSupportedException) {
            code = HttpStatus.SC_NOT_IMPLEMENTED;
        } else if (ex instanceof UnsupportedHttpVersionException) {
            code = HttpStatus.SC_HTTP_VERSION_NOT_SUPPORTED;
        } else if (ex instanceof ProtocolException) {
            code = HttpStatus.SC_BAD_REQUEST;
        }
        response.setStatusCode(code);

        final byte[] msg = EncodingUtils.getAsciiBytes(ex.getMessage());
        final NByteArrayEntity entity = new NByteArrayEntity(msg);
        entity.setContentType("text/plain; charset=US-ASCII");
        response.setEntity(entity);
    }

    /**
     * @throws HttpException - not thrown currently
     */
    private void processRequest(
            final NHttpServerConnection conn,
            final HttpRequest request) throws IOException, HttpException {

        final HttpContext context = conn.getContext();
        final ServerConnState connState = (ServerConnState) context.getAttribute(CONN_STATE);

        ProtocolVersion ver = request.getRequestLine().getProtocolVersion();

        if (!ver.lessEquals(HttpVersion.HTTP_1_1)) {
            // Downgrade protocol version if greater than HTTP/1.1
            ver = HttpVersion.HTTP_1_1;
        }

        final NHttpResponseTrigger trigger = new ResponseTriggerImpl(connState, conn);
        try {
            this.httpProcessor.process(request, context);

            final NHttpRequestHandler handler = connState.getRequestHandler();
            if (handler != null) {
                final HttpResponse response = this.responseFactory.newHttpResponse(
                        ver, HttpStatus.SC_OK, context);
                response.setParams(
                        new DefaultedHttpParams(response.getParams(), this.params));

                handler.handle(
                        request,
                        response,
                        trigger,
                        context);
            } else {
                final HttpResponse response = this.responseFactory.newHttpResponse(ver,
                        HttpStatus.SC_NOT_IMPLEMENTED, context);
                response.setParams(
                        new DefaultedHttpParams(response.getParams(), this.params));
                trigger.submitResponse(response);
            }

        } catch (final HttpException ex) {
            trigger.handleException(ex);
        }
    }

    private void sendResponse(
            final NHttpServerConnection conn,
            final HttpRequest request,
            final HttpResponse response) throws IOException, HttpException {
        final HttpContext context = conn.getContext();
        final ServerConnState connState = (ServerConnState) context.getAttribute(CONN_STATE);

        // Now that a response is ready, we can cleanup the listener for the request.
        connState.finishInput();

        // Some processers need the request that generated this response.
        context.setAttribute(ExecutionContext.HTTP_REQUEST, request);
        this.httpProcessor.process(response, context);
        context.setAttribute(ExecutionContext.HTTP_REQUEST, null);

        if (response.getEntity() != null && !canResponseHaveBody(request, response)) {
            response.setEntity(null);
        }

        final HttpEntity entity = response.getEntity();
        if (entity != null) {
            if (entity instanceof ProducingNHttpEntity) {
                connState.setProducingEntity((ProducingNHttpEntity) entity);
            } else {
                connState.setProducingEntity(new NHttpEntityWrapper(entity));
            }
        }

        conn.submitResponse(response);

        if (entity == null) {
            if (!this.connStrategy.keepAlive(response, context)) {
                conn.close();
            } else {
                // Ready to process new request
                connState.reset();
                conn.requestInput();
            }
            responseComplete(response, context);
        }
    }

    /**
     * Signals that this response has been fully sent. This will be called after
     * submitting the response to a connection, if there is no entity in the
     * response. If there is an entity, it will be called after the entity has
     * completed.
     */
    protected void responseComplete(final HttpResponse response, final HttpContext context) {
    }

    private NHttpRequestHandler getRequestHandler(final HttpRequest request) {
        NHttpRequestHandler handler = null;
         if (this.handlerResolver != null) {
             final String requestURI = request.getRequestLine().getUri();
             handler = this.handlerResolver.lookup(requestURI);
         }

         return handler;
    }

    protected static class ServerConnState {

        private volatile NHttpRequestHandler requestHandler;
        private volatile HttpRequest request;
        private volatile ConsumingNHttpEntity consumingEntity;
        private volatile HttpResponse response;
        private volatile ProducingNHttpEntity producingEntity;
        private volatile IOException ioex;
        private volatile HttpException httpex;
        private volatile boolean handled;

        public void finishInput() throws IOException {
            if (this.consumingEntity != null) {
                this.consumingEntity.finish();
                this.consumingEntity = null;
            }
        }

        public void finishOutput() throws IOException {
            if (this.producingEntity != null) {
                this.producingEntity.finish();
                this.producingEntity = null;
            }
        }

        public void reset() throws IOException {
            finishInput();
            this.request = null;
            finishOutput();
            this.handled = false;
            this.response = null;
            this.ioex = null;
            this.httpex = null;
            this.requestHandler = null;
        }

        public NHttpRequestHandler getRequestHandler() {
            return this.requestHandler;
        }

        public void setRequestHandler(final NHttpRequestHandler requestHandler) {
            this.requestHandler = requestHandler;
        }

        public HttpRequest getRequest() {
            return this.request;
        }

        public void setRequest(final HttpRequest request) {
            this.request = request;
        }

        public ConsumingNHttpEntity getConsumingEntity() {
            return this.consumingEntity;
        }

        public void setConsumingEntity(final ConsumingNHttpEntity consumingEntity) {
            this.consumingEntity = consumingEntity;
        }

        public HttpResponse getResponse() {
            return this.response;
        }

        public void setResponse(final HttpResponse response) {
            this.response = response;
        }

        public ProducingNHttpEntity getProducingEntity() {
            return this.producingEntity;
        }

        public void setProducingEntity(final ProducingNHttpEntity producingEntity) {
            this.producingEntity = producingEntity;
        }

        public IOException getIOException() {
            return this.ioex;
        }

        public IOException getIOExepction() {
            return this.ioex;
        }

        public void setIOException(final IOException ex) {
            this.ioex = ex;
        }

        public void setIOExepction(final IOException ex) {
            this.ioex = ex;
        }

        public HttpException getHttpException() {
            return this.httpex;
        }

        public HttpException getHttpExepction() {
            return this.httpex;
        }

        public void setHttpException(final HttpException ex) {
            this.httpex = ex;
        }

        public void setHttpExepction(final HttpException ex) {
            this.httpex = ex;
        }

        public boolean isHandled() {
            return this.handled;
        }

        public void setHandled(final boolean handled) {
            this.handled = handled;
        }

    }

    private static class ResponseTriggerImpl implements NHttpResponseTrigger {

        private final ServerConnState connState;
        private final IOControl iocontrol;

        private volatile boolean triggered;

        public ResponseTriggerImpl(final ServerConnState connState, final IOControl iocontrol) {
            super();
            this.connState = connState;
            this.iocontrol = iocontrol;
        }

        @Override
        public void submitResponse(final HttpResponse response) {
            Args.notNull(response, "Response");
            Asserts.check(!this.triggered, "Response already triggered");
            this.triggered = true;
            this.connState.setResponse(response);
            this.iocontrol.requestOutput();
        }

        @Override
        public void handleException(final HttpException ex) {
            Asserts.check(!this.triggered, "Response already triggered");
            this.triggered = true;
            this.connState.setHttpException(ex);
            this.iocontrol.requestOutput();
        }

        @Override
        public void handleException(final IOException ex) {
            Asserts.check(!this.triggered, "Response already triggered");
            this.triggered = true;
            this.connState.setIOException(ex);
            this.iocontrol.requestOutput();
        }

    }

}
