All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mastfrog.acteur.ResponseImpl Maven / Gradle / Ivy

There is a newer version: 2.9.7
Show newest version
/*
 * The MIT License
 *
 * Copyright 2014 Tim Boudreau.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.mastfrog.acteur;

import com.google.common.base.Objects;
import com.google.common.collect.Sets;
import com.google.common.net.MediaType;
import com.google.inject.Key;
import com.google.inject.name.Names;
import com.mastfrog.acteur.ResponseWriter.AbstractOutput;
import com.mastfrog.acteur.ResponseWriter.Output;
import com.mastfrog.acteur.ResponseWriter.Status;
import com.mastfrog.acteur.headers.HeaderValueType;
import com.mastfrog.acteur.headers.Headers;
import static com.mastfrog.acteur.headers.Headers.CONTENT_TYPE;
import com.mastfrog.acteur.headers.Method;
import com.mastfrog.acteur.server.ServerModule;
import static com.mastfrog.acteur.server.ServerModule.X_INTERNAL_COMPRESS;
import com.mastfrog.acteur.spi.ApplicationControl;
import com.mastfrog.giulius.Dependencies;
import com.mastfrog.giulius.scope.ReentrantScope;
import com.mastfrog.marshallers.netty.NettyContentMarshallers;
import com.mastfrog.util.preconditions.Checks;
import com.mastfrog.util.codec.Codec;
import com.mastfrog.util.strings.Strings;
import com.mastfrog.util.collections.CollectionUtils;
import com.mastfrog.util.thread.ThreadLocalTransfer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import static io.netty.channel.ChannelFutureListener.CLOSE;
import io.netty.channel.FileRegion;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.TRANSFER_ENCODING;
import io.netty.handler.codec.http.HttpHeaderValues;
import static io.netty.handler.codec.http.HttpHeaderValues.IDENTITY;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED;
import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import io.netty.handler.codec.http.HttpVersion;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_0;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.util.AsciiString;
import java.io.IOException;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Aggregates the set of headers and a body writer which is used to respond to
 * an HTTP request. Each Acteur has its own which will be merged into the one
 * belonging to the page if it succeeds, so Acteurs that reject a response
 * cannot have side-effects.
 *
 * @author Tim Boudreau
 */
final class ResponseImpl extends Response {

    private volatile boolean modified;
    HttpResponseStatus status;
    private final List> headers = new ArrayList<>(3);
    private Object message;
    ChannelFutureListener listener;
    private boolean chunked;
    private Duration delay;
    private static final boolean debug = Boolean.getBoolean("acteur.debug");

    static final ThreadLocalTransfer> shadowResponses = new ThreadLocalTransfer<>();
    List alsoConsult;

    ResponseImpl() {
        // Ensure's an Acteur's call to response().get(Headers.FOO) can see
        // values set earlier in the chain.  Needed to get rid of ResponseHeaders
        // and page.decorateResponse().
        List previousActeursResponses = shadowResponses.get();
        if (previousActeursResponses != null) {
            alsoConsult = CollectionUtils.reversed(previousActeursResponses);
        }
    }

    boolean hasListener() {
        return listener != null;
    }

    Object message() {
        return message;
    }

    boolean isModified() {
        return modified;
    }

    void modify() {
        this.modified = true;
    }

    void merge(ResponseImpl other) {
        Checks.notNull("other", other);
        this.modified |= other.modified;
        if (other.modified) {
            for (Entry e : other.headers) {
                addEntry(e);
            }
            if (other.status != null) {
                status(other.status);
            }
            if (other.message != null) {
                content(other.message);
            }
            if (other.chunked) {
                chunked(true);
            }
            if (other.listener != null) {
                contentWriter(other.listener);
            }
            if (other.delay != null) {
                this.delay = other.delay;
            }
        }
    }

    private  void addEntry(Entry e) {
        add(e.decorator, e.value);
    }

    @Override
    public Response content(Object message) {
        modify();
        this.message = message;
        return this;
    }

    @Override
    public Response delayedBy(Duration delay) {
        modify();
        this.delay = delay;
        return this;
    }

    @Override
    public Response status(HttpResponseStatus status) {
        modify();
        this.status = status;
        return this;
    }

    HttpResponseStatus rawStatus() {
        return status;
    }

    public HttpResponseStatus getResponseCode() {
        if (status == null && alsoConsult != null) {
            for (ResponseImpl previous : alsoConsult) {
                HttpResponseStatus raw = previous.rawStatus();
                if (raw != null) {
                    return raw;
                }
            }
        }
        return status == null ? HttpResponseStatus.OK : status;
    }

    @Override
    public Response contentWriter(ResponseWriter writer) {
        Page p = Page.get();
        Application app = p.getApplication();
        Dependencies deps = app.getDependencies();
        HttpEvent evt = deps.getInstance(HttpEvent.class);
        Charset charset = deps.getInstance(Charset.class);
        ByteBufAllocator allocator = deps.getInstance(ByteBufAllocator.class);
        Codec mapper = deps.getInstance(Codec.class);
        Key key = Key.get(ExecutorService.class,
                Names.named(ServerModule.WORKER_THREAD_POOL_NAME));
        ExecutorService svc = deps.getInstance(key);
        setWriter(writer, charset, allocator, mapper, evt, svc, app.control());
        return this;
    }

    Duration getDelay() {
        return delay;
    }

    @SuppressWarnings("deprecation")
    private String cookieName(Object o) {
        if (o instanceof Cookie) {
            return ((Cookie) o).name();
        } else if (o instanceof io.netty.handler.codec.http.Cookie) {
            return ((io.netty.handler.codec.http.Cookie) o).name();
        } else {
            return null;
        }
    }

    private boolean compareCookies(Object old, Object nue) {
        return Objects.equal(cookieName(old), cookieName(nue));
    }

    @SuppressWarnings("unchecked")
    @Override
    public  Response add(HeaderValueType decorator, T value) {
        List> old = new LinkedList<>();
        // XXX set cookie!
        for (Iterator> it = headers.iterator(); it.hasNext();) {
            Entry e = it.next();
            // Do prune setting the same cookie twice
            if (decorator.is(HttpHeaderNames.SET_COOKIE)
                    && e.decorator.is(HttpHeaderNames.SET_COOKIE)) {
                if (compareCookies(e.value, value)) {
                    it.remove();
                    continue;
                } else {
                    continue;
                }
            }
            if (e.match(decorator) != null) {
                old.add(e);
                it.remove();
            }
        }
        Entry e = new Entry<>(decorator, value);
        // For now, special handling for Allow:
        // Longer term, should HeaderValueType.isArray() and a way to
        // coalesce
        if (!old.isEmpty() && decorator.is(HttpHeaderNames.ALLOW)) {
            old.add(e);
            Set all = EnumSet.noneOf(Method.class);
            for (Entry en : old) {
                Method[] m = (Method[]) en.value;
                all.addAll(Arrays.asList(m));
            }
            value = (T) all.toArray(new Method[all.size()]);
            e = new Entry<>(decorator, value);
        }
        headers.add(e);
        modify();
        return this;
    }

    @Override
    public  T get(HeaderValueType decorator) {
        T result = internalGet(decorator);
        if (result == null && alsoConsult != null) {
            for (ResponseImpl preceding : alsoConsult) {
                result = preceding.internalGet(decorator);
                if (result != null) {
                    break;
                }
            }
        }
        return result;
    }

     T internalGet(HeaderValueType headerType) {
        for (Entry e : headers) {
            HeaderValueType d = e.match(headerType);
            if (d != null) {
                return d.type().cast(e.value);
            }
        }
        return null;
    }

    @Override
    public Response chunked(boolean chunked) {
        if (chunked != this.chunked) {
            this.chunked = chunked;
            modify();
        }
        return this;
    }

     void setWriter(T w, Dependencies deps, HttpEvent evt) {
        Charset charset = deps.getInstance(Charset.class);
        ByteBufAllocator allocator = deps.getInstance(ByteBufAllocator.class);
        Codec mapper = deps.getInstance(Codec.class);
        Key key = Key.get(ExecutorService.class, Names.named(ServerModule.WORKER_THREAD_POOL_NAME));
        ExecutorService svc = deps.getInstance(key);
        ApplicationControl ctrl = deps.getInstance(ApplicationControl.class);
        setWriter(w, charset, allocator, mapper, evt, svc, ctrl);
    }

     void setWriter(Class w, Dependencies deps, HttpEvent evt) {
        Charset charset = deps.getInstance(Charset.class);
        ByteBufAllocator allocator = deps.getInstance(ByteBufAllocator.class);
        Key key = Key.get(ExecutorService.class, Names.named(ServerModule.WORKER_THREAD_POOL_NAME));
        ExecutorService svc = deps.getInstance(key);
        Codec mapper = deps.getInstance(Codec.class);
        setWriter(new DynResponseWriter(w, deps), charset, allocator, mapper, evt, svc,
                deps.getInstance(ApplicationControl.class));
    }

    static class DynResponseWriter extends ResponseWriter {

        private final AtomicReference actual = new AtomicReference<>();
        private final Callable resp;

        DynResponseWriter(final Class type, final Dependencies deps) {
            ReentrantScope scope = deps.getInstance(ReentrantScope.class);
            assert scope.inScope();
            resp = scope.wrap(new Callable() {

                @Override
                public ResponseWriter call() throws Exception {
                    ResponseWriter w = actual.get();
                    if (w == null) {
                        actual.set(w = deps.getInstance(type));
                    }
                    return w;
                }

                public String toString() {
                    return type.toString();
                }
            });
        }

        @Override
        public ResponseWriter.Status write(Event evt, Output out) throws Exception {
            ResponseWriter actual = resp.call();
            return actual.write(evt, out);
        }

        @Override
        public Status write(Event evt, Output out, int iteration) throws Exception {
            ResponseWriter actual = resp.call();
            return actual.write(evt, out, iteration);
        }

        public String toString() {
            return "DynResponseWriter for " + resp.toString();
        }
    }

    private boolean hasTransferEncodingChunked() {
        for (Entry entry : headers) {
            if (Headers.TRANSFER_ENCODING.is(entry.decorator.name())) {
                return Strings.charSequencesEqual(HttpHeaderValues.CHUNKED, entry.stringValue(), true);
            }
        }
        return false;
    }

    private boolean hasContentLength() {
        for (Entry entry : headers) {
            if (Headers.CONTENT_LENGTH.equals(entry.decorator)) {
                return Strings.charSequencesEqual(HttpHeaderValues.CHUNKED, entry.stringValue(), true);
            }
        }
        return false;

    }

    private boolean isHttp10StyleResponse() {
        // If no content length is set, but a listener is present, and transfer encoding
        // is not set, then we are going to fire raw ByteBufs down the pipe and we need
        // to close the connection to indicate that the response is finished
        return listener != null && !chunked && !hasTransferEncodingChunked() && !hasContentLength();
    }

    boolean isKeepAlive(Event evt) {
        boolean result = evt instanceof HttpEvent ? ((HttpEvent) evt).requestsConnectionStayOpen() : false;
        if (result) {
            result = !isHttp10StyleResponse();
        }
        return result;
    }

    void setWriter(ResponseWriter w, Charset charset, ByteBufAllocator allocator,
            Codec mapper, Event evt, ExecutorService svc, ApplicationControl ctrl) {
        contentWriter(new ResponseWriterListener(evt, w, charset, allocator,
                mapper, chunked, !isKeepAlive(evt), svc, ctrl));
    }

    String listenerString() {
        if (listener != null) {
            if (listener instanceof ResponseWriterListener) {
                return ((ResponseWriterListener) listener).writer.getClass().getName();
            }
            if (listener instanceof Acteur.ScopeWrapper || listener instanceof Acteur.IWrapper) {
                return listener.toString();
            }
            return listener.getClass().getName();
        }
        return "";
    }

    private static final class ResponseWriterListener extends AbstractOutput implements ChannelFutureListener {

        private volatile ChannelFuture future;
        private volatile int callCount = 0;
        private final boolean chunked;
        final ResponseWriter writer;
        private final boolean shouldClose;
        private final Event evt;
        private final ExecutorService svc;
        private final ApplicationControl ctrl;

        ResponseWriterListener(Event evt, ResponseWriter writer, Charset charset,
                ByteBufAllocator allocator, Codec mapper, boolean chunked,
                boolean shouldClose, ExecutorService svc, ApplicationControl ctrl) {
            super(charset, allocator, mapper);
            this.chunked = chunked;
            this.writer = writer;
            this.shouldClose = shouldClose;
            this.evt = evt;
            this.svc = svc;
            this.ctrl = ctrl;
        }

        @Override
        public Channel channel() {
            if (future == null) {
                throw new IllegalStateException("No future -> no channel");
            }
            return future.channel();
        }

        @Override
        public Output write(ByteBuf buf) throws IOException {
            assert future != null;
            if (chunked) {
                future = future.channel().writeAndFlush(new DefaultHttpContent(buf));
            } else {
                future = future.channel().writeAndFlush(buf);
            }
            return this;
        }

        volatile boolean inOperationComplete;
        volatile int entryCount = 0;

        @Override
        public void operationComplete(final ChannelFuture future) throws Exception {
            if (future.cause() != null) {
                ctrl.internalOnError(future.cause());
                if (future.channel() != null && future.channel().isOpen()) {
                    future.channel().close();
                }
                return;
            }
            try {
                // See https://github.com/netty/netty/issues/2415 for why this is needed
                if (entryCount > 0) {
                    svc.submit(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                operationComplete(future);
                            } catch (Exception ex) {
                                ctrl.internalOnError(ex);
                                if (future != null && future.channel().isOpen()) {
                                    future.channel().close();
                                }
                            }
                        }
                    });
                    return;
                }
                entryCount++;
                Callable c = new Callable() {
                    @Override
                    public Void call() throws Exception {
                        inOperationComplete = true;
                        try {
                            ResponseWriterListener.this.future = future;
                            ResponseWriter.Status status = writer.write(evt, ResponseWriterListener.this, callCount++);
                            if (status.isCallback()) {
                                ResponseWriterListener.this.future = ResponseWriterListener.this.future.addListener(ResponseWriterListener.this);
                            } else if (status == Status.DONE) {
                                if (chunked) {
                                    ResponseWriterListener.this.future = ResponseWriterListener.this.future.channel().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
                                }
                                if (shouldClose) {
                                    ResponseWriterListener.this.future = ResponseWriterListener.this.future.addListener(CLOSE);
                                }
                            }
                        } catch (Exception ex) {
                            ctrl.internalOnError(ex);
                        } finally {
                            inOperationComplete = false;
                        }
                        return null;
                    }
                };
                if (!inOperationComplete) {
                    c.call();
                } else {
                    svc.submit(c);
                }
            } finally {
                entryCount--;
            }
        }

        @Override
        public ChannelFuture future() {
            return future;
        }

        @Override
        public Output write(HttpContent chunk) throws IOException {
            if (!chunked) {
                ResponseWriterListener.this.future = ResponseWriterListener.this.future.channel().writeAndFlush(chunk.content());
            } else {
                ResponseWriterListener.this.future = ResponseWriterListener.this.future.channel().writeAndFlush(chunk);
            }
            return this;
        }

        @Override
        public Output write(FileRegion region) throws IOException {
            ResponseWriterListener.this.future = ResponseWriterListener.this.future.channel().writeAndFlush(region);
            if (shouldClose) {
                future.addListener(CLOSE);
            }
            return this;
        }
    }

    /**
     * Set a ChannelFutureListener which will be called after headers are
     * written and flushed to the socket; prefer
     * setResponseWriter() to this method unless you are not using
     * chunked encoding and want to stream your response (in which case, be sure
     * to chunked(false) or you will have encoding errors).
     *
     * @param listener
     */
    @Override
    public Response contentWriter(ChannelFutureListener listener) {
        if (this.listener != null) {
            throw new IllegalStateException("Listener already set to " + this.listener);
        }
        this.listener = listener;
        return this;
    }

    public Object getMessage() {
        return message;
    }

    final boolean canHaveBody(HttpResponseStatus status) {
        switch (status.code()) {
            case 204:
            case 205:
            case 304:
                return false;
            default:
                return true;
        }
    }

    private ByteBuf writeMessage(Event evt, Charset charset) throws Exception {
        if (message == null) {
            return null;
        }
        if (message instanceof ByteBuf) {
            return (ByteBuf) message;
        }
        Page p = Page.get();
        if (p == null) {
            throw new IllegalStateException("Call to write message with Page.set() not called (outside request scope?)");
        }
        NettyContentMarshallers marshallers = p.getApplication().getDependencies().getInstance(NettyContentMarshallers.class);
        ByteBuf buf = evt.channel().alloc().ioBuffer();
        marshallers.write(message, buf, charset);
        return buf;
    }

    HttpResponseStatus internalStatus() {
        return status == null ? OK : status;
    }

    private boolean has(CharSequence headerName) {
        for (Entry e : this.headers) {
            if (e.is(headerName)) {
                return true;
            }
        }
        return false;
    }

    boolean hasNoPayload() {
        HttpResponseStatus status = internalStatus();
        if (status == NOT_MODIFIED || status == NO_CONTENT) {
            return true;
        }
        if (listener == null && (message == null || (message instanceof String && ((String) message).isEmpty()))) {
            return true;
        }
        return false;
    }

    private static final AsciiString ZERO = AsciiString.of("0");
    public HttpResponse toResponse(Event evt, Charset defaultCharset) throws Exception {
        HttpResponseStatus status = internalStatus();
        // Log cases where a payload is attached to a status code that cannot have a payload
        // according to the HTTP spec
        if (!canHaveBody(status) && (message != null || listener != null)) {
            if (listener != ChannelFutureListener.CLOSE && listener != SEND_EMPTY_LAST_CHUNK) {
                System.err.println(evt
                        + " attempts to attach a body to " + status
                        + " which cannot have one: " + message
                        + " - " + listener);
            }
        }
        // Ensure we pass the correct character set based on the MIME type and failing
        // over to the character set the application was configured with (default UTF-8):
        MediaType mimeType = get(CONTENT_TYPE);
        if (mimeType != null && mimeType.charset().isPresent()) {
            defaultCharset = mimeType.charset().get();
        }
        // Convert the message payload, if any, into a ByteBuf
        ByteBuf buf = writeMessage(evt, defaultCharset);
        // If this happens, the application is telling the framework to do two contradictory things -
        // you can either send a payload by attaching it to the response, or by attaching a listener
        // which will be notified when the headers have been written (or flushed) to the socket
        // buffer - but not both
        if (buf != null && listener != null) {
            throw new IllegalStateException("Both outbound buffer, and listener for header flush are present;"
                    + " either one can write the response body, but not both");
        }
        // Start constructing the actual HTTP response
        DefaultHttpHeaders hdrs = new DefaultHttpHeaders();
        // Figure out if this response cannot possibly be anything more than headers
        boolean noBody = (listener == null && buf == null) || status == NO_CONTENT || status == NOT_MODIFIED;
        boolean hasInternalCompress = has(X_INTERNAL_COMPRESS);
        boolean hasContentEncoding = has(CONTENT_ENCODING);
        boolean hasTransferEncoding = has(TRANSFER_ENCODING);
        boolean hasContentLength = false;

        // To ensure the compressor doesn't screw with things it shouldn't - we have a buffer with
        // the complete response.
        boolean addIdentityContentEncoding = buf != null && !hasContentEncoding && !hasInternalCompress;
        // Iterate and filter the headers to create a usable response
        for (Entry e : headers) {
            if (noBody) {
                // Ensure a response with no body is always a zero content-length response; discard content-length
                // as it doesn't belong in a chunked response;  content encoding is irrelevant with no
                // content, and same with transfer-encoding
                if (e.is(CONTENT_LENGTH)) {
                    continue;
                } else if (e.is(CONTENT_ENCODING)) {
                    hasContentEncoding = false;
                    continue;
                } else if (e.is(TRANSFER_ENCODING)) {
                    hasTransferEncoding = false;
                    continue;
                }
            }
            // Avoid double content-encoding headers
            if (addIdentityContentEncoding && e.is(CONTENT_ENCODING)) {
                continue;
            }
            hasContentLength |= e.is(CONTENT_LENGTH);
            e.write(hdrs);
        }
        // Some debug logging
        if (debug && evt instanceof HttpEvent) {
            System.out.println("\n\n********************\n"
                    + ((HttpEvent) evt).path() + " " + status + " hasInternalCompress "
                    + hasInternalCompress + " hasContentLength " + hasContentLength
                    + " hasTransferEncoding " + hasTransferEncoding + " hasContentEncoding " + hasContentEncoding
                    + " chunked " + chunked + " noBody " + noBody);
        }
        if (addIdentityContentEncoding) {
            hdrs.add(CONTENT_ENCODING, IDENTITY);
        }
        if (noBody) {
            // Return the buffer's memory to the pool if present
            if (buf != null) {
                buf.release();
            }
            // Using DefaultFullHttpResponse, especially with 0-byte responses,
            // leaves the encoder in a bad state, where it will throw an exception
            // if the connection is reused for another response.  So instead, we
            // manually flush our own last content

//            hdrs.set(TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
//            listener = SEND_EMPTY_LAST_CHUNK;
//            chunked = true;
            if (debug) {
                System.out.println("noBody - set content-length: 0, chunked false");
            }
            hdrs.set(CONTENT_LENGTH, ZERO);
            chunked = false;
            return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.EMPTY_BUFFER, hdrs, EmptyHttpHeaders.INSTANCE);

        } else {
            if (chunked) {
                if (!hasTransferEncoding) {
//                    System.out.println("chunked and to xfer - set transfer encoding to chunked");
                    hdrs.set(TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
                }
            } else if (buf != null) {
                hdrs.set(CONTENT_LENGTH, buf.readableBytes());
//                System.out.println("set content-length to " + buf.readableBytes());
            }
        }
        HttpVersion version = HTTP_1_1;
        if (!chunked && buf == null && listener != null && !hasContentEncoding) {
            if (!hasContentLength && listener != CLOSE && listener != SEND_EMPTY_LAST_CHUNK) {
                // Unless an HTTP 1.0 style response is really desired, this usually
                // indicates a bug, and the connection to the browser will hang
                // indefinitely at the end of the request
                warn(evt);
                version = HTTP_1_0;
            }
            System.out.println("Set X-Internal-Compress");
            hdrs.set(X_INTERNAL_COMPRESS, true);
        }
        if (debug) {
            System.out.println(" final headers " + headersString(hdrs));
        }
        if (buf != null) {
//            return new DefaultFullHttpResponse(HTTP_1_1, status, buf, hdrs, EmptyHttpHeaders.INSTANCE);
            if (debug) {
                System.out.println("  has a buffer, send it as a chunk");
            }
            listener = new SendOneBuffer(buf);
            return new DefaultHttpResponse(version, status, hdrs);
        } else {
            return new DefaultHttpResponse(version, status, hdrs);
        }
    }

    static Set WARNED = Sets.newConcurrentHashSet();

    static void warn(Event evt) {
        String pth = evt instanceof HttpEvent ? ((HttpEvent) evt).path().toString() : "";
        if (!WARNED.contains(pth)) {
            WARNED.add(pth);
            System.err.println("Response to " + pth + " is non-chunked, has no content-length header but "
                    + "will send response chunks using a listener.  The only way to avoid hanging "
                    + "the client is to close the connection, HTTP 1.0 style, once all data is sent.");
        }
    }

    private String headersString(HttpHeaders resp) {
        StringBuilder sb = new StringBuilder();
        if (resp != null) {
            for (Map.Entry e : resp.entries()) {
                sb.append('\n').append(e.getKey()).append(": ").append(e.getValue());
            }
        }
        return sb.toString();
    }

    static final class SendOneBuffer implements ChannelFutureListener {

        private final ByteBuf buf;

        public SendOneBuffer(ByteBuf buf) {
            this.buf = buf;
        }

        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            if (!future.isDone() || future.isSuccess()) {
                future.channel().writeAndFlush(new DefaultLastHttpContent(buf));
            }
        }

    }
    private static final AsciiString TRUE = AsciiString.of("1");

    static final SendEmptyLastChunk SEND_EMPTY_LAST_CHUNK = new SendEmptyLastChunk();

    static final class SendEmptyLastChunk implements ChannelFutureListener {

        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            if (!future.isDone() || future.isSuccess()) {
                future.channel().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            } else {
                if (future.cause() != null) {
                    future.cause().printStackTrace();
                }
            }
        }

    }

    ChannelFuture sendMessage(Event evt, ChannelFuture future, HttpMessage resp, boolean trigger) throws Exception {
        if (listener != null) {
            if (trigger) {
                listener.operationComplete(future);
            } else {
                future = future.addListener(listener);
            }
            return future;
        } else if (!isKeepAlive(evt)) {
            future = future.addListener(ChannelFutureListener.CLOSE);
        }
        return future;
    }

    @Override
    public String toString() {
        return "Response{" + "modified=" + modified + ", status="
                + status + ", headers=" + headers + ", message=" + message
                + ", listener=" + listener + ", chunked=" + chunked
                + '}';
    }

    private static final class Entry {

        private final HeaderValueType decorator;
        private final T value;

        Entry(HeaderValueType decorator, T value) {
            Checks.notNull("decorator", decorator);
            Checks.notNull(decorator.name().toString(), value);
            this.decorator = decorator;
            this.value = value;
        }

        public void decorate(HttpMessage msg) {
            msg.headers().set(decorator.name(), value);
        }

        public boolean is(CharSequence name) {
            return decorator.is(name);
        }

        public void write(HttpMessage msg) {
            Headers.write(decorator, value, msg);
        }

        void write(HttpHeaders headers) {
            Headers.write(decorator, value, headers);
        }

        public CharSequence stringValue() {
            return decorator.toCharSequence(value);
        }

        @Override
        public String toString() {
            return decorator.name() + ": " + decorator.toCharSequence(value);
        }

        @Override
        public int hashCode() {
            return decorator.name().hashCode();
        }

        @Override
        public boolean equals(Object o) {
            return o instanceof Entry && ((Entry) o).decorator.name().equals(decorator.name());
        }

        @SuppressWarnings({"unchecked"})
        public  HeaderValueType match(HeaderValueType decorator) {
            if (this.decorator.equals(decorator)) { // Equality test is case-insensitive name match
                if (this.decorator.type() != decorator.type()) {
                    System.err.println("Requesting header " + decorator + " of type " + decorator.type().getName()
                            + " but returning header of type " + this.decorator.type().getName() + " - if set, this"
                            + " will probably throw a ClassCastException.");
                }
                return (HeaderValueType) this.decorator;
            }
            if (this.decorator.name().equals(decorator.name())
                    && this.decorator.type().equals(decorator.type())) {
                return decorator;
            } else if (Strings.charSequencesEqual(this.decorator.name(), decorator.name(), true)) {

            }
            return null;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy