com.linecorp.armeria.internal.http.Http1ObjectEncoder.java Source code

Java tutorial

Introduction

Here is the source code for com.linecorp.armeria.internal.http.Http1ObjectEncoder.java

Source

/*
 * Copyright 2016 LINE Corporation
 *
 * LINE Corporation 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.
 */

package com.linecorp.armeria.internal.http;

import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayDeque;
import java.util.Map.Entry;
import java.util.Queue;

import com.linecorp.armeria.common.ClosedSessionException;
import com.linecorp.armeria.common.http.HttpData;
import com.linecorp.armeria.common.http.HttpHeaders;
import com.linecorp.armeria.common.http.HttpMethod;
import com.linecorp.armeria.common.http.HttpStatus;
import com.linecorp.armeria.common.http.HttpStatusClass;
import com.linecorp.armeria.common.reactivestreams.ClosedPublisherException;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http2.Http2Error;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;

public final class Http1ObjectEncoder extends HttpObjectEncoder {

    private final boolean server;

    /**
     * The ID of the request which is at its turn to send a response.
     */
    private int currentId = 1;

    /**
     * The minimum ID of the request whose stream has been closed/reset.
     */
    private int minClosedId = Integer.MAX_VALUE;

    /**
     * The maximum known ID with pending writes.
     */
    private int maxIdWithPendingWrites = Integer.MIN_VALUE;

    /**
     * The map which maps a request ID to its related pending response.
     */
    private final IntObjectMap<PendingWrites> pendingWrites = new IntObjectHashMap<>();

    public Http1ObjectEncoder(boolean server) {
        this.server = server;
    }

    @Override
    protected ChannelFuture doWriteHeaders(ChannelHandlerContext ctx, int id, int streamId, HttpHeaders headers,
            boolean endStream) {
        if (id >= minClosedId) {
            return ctx.newFailedFuture(ClosedSessionException.get());
        }

        final HttpObject converted;
        try {
            converted = server ? convertServerHeaders(streamId, headers, endStream)
                    : convertClientHeaders(streamId, headers);

            return write(ctx, id, converted, endStream);
        } catch (Throwable t) {
            return ctx.newFailedFuture(t);
        }
    }

    private HttpObject convertServerHeaders(int streamId, HttpHeaders headers, boolean endStream)
            throws Http2Exception {

        // Leading headers will always have :status, trailers will never have it.
        final HttpStatus status = headers.status();
        if (status == null) {
            return convertTrailingHeaders(streamId, headers);
        }

        // Convert leading headers.
        final HttpResponse res;
        final boolean informational = status.codeClass() == HttpStatusClass.INFORMATIONAL;

        if (endStream || informational) {

            res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status.toNettyStatus(), Unpooled.EMPTY_BUFFER,
                    false);

            final io.netty.handler.codec.http.HttpHeaders outHeaders = res.headers();
            convert(streamId, headers, outHeaders, false);

            if (informational) {
                // 1xx responses does not have the 'content-length' header.
                outHeaders.remove(HttpHeaderNames.CONTENT_LENGTH);
            } else if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
                // NB: Set the 'content-length' only when not set rather than always setting to 0.
                //     It's because a response to a HEAD request can have empty content while having
                //     non-zero 'content-length' header.
                //     However, this also opens the possibility of sending a non-zero 'content-length'
                //     header even when it really has to be zero. e.g. a response to a non-HEAD request
                outHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
            }
        } else {
            res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status.toNettyStatus(), false);
            // Perform conversion.
            convert(streamId, headers, res.headers(), false);
            setTransferEncoding(res);
        }

        return res;
    }

    private HttpObject convertClientHeaders(int streamId, HttpHeaders headers) throws Http2Exception {

        // Leading headers will always have :method, trailers will never have it.
        final HttpMethod method = headers.method();
        if (method == null) {
            return convertTrailingHeaders(streamId, headers);
        }

        // Convert leading headers.
        final HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1, method.toNettyMethod(), headers.path(),
                false);

        convert(streamId, headers, req.headers(), false);
        if (HttpUtil.getContentLength(req, -1L) >= 0) {
            // Avoid the case where both 'content-length' and 'transfer-encoding' are set.
            req.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
        } else {
            req.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
        }

        return req;
    }

    private void convert(int streamId, HttpHeaders inHeaders, io.netty.handler.codec.http.HttpHeaders outHeaders,
            boolean trailer) throws Http2Exception {

        ArmeriaHttpUtil.toNettyHttp1(streamId, inHeaders, outHeaders, HttpVersion.HTTP_1_1, trailer, false);

        outHeaders.remove(ExtensionHeaderNames.STREAM_ID.text());
        if (server) {
            outHeaders.remove(ExtensionHeaderNames.SCHEME.text());
        } else {
            outHeaders.remove(ExtensionHeaderNames.PATH.text());
        }
    }

    private LastHttpContent convertTrailingHeaders(int streamId, HttpHeaders headers) throws Http2Exception {
        final LastHttpContent lastContent;
        if (headers.isEmpty()) {
            lastContent = LastHttpContent.EMPTY_LAST_CONTENT;
        } else {
            lastContent = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, false);
            convert(streamId, headers, lastContent.trailingHeaders(), true);
        }
        return lastContent;
    }

    private static void setTransferEncoding(HttpMessage out) {
        final io.netty.handler.codec.http.HttpHeaders outHeaders = out.headers();
        final long contentLength = HttpUtil.getContentLength(out, -1L);
        if (contentLength < 0) {
            // Use chunked encoding.
            outHeaders.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
            outHeaders.remove(HttpHeaderNames.CONTENT_LENGTH);
        }
    }

    @Override
    protected ChannelFuture doWriteData(ChannelHandlerContext ctx, int id, int streamId, HttpData data,
            boolean endStream) {

        if (id >= minClosedId) {
            return ctx.newFailedFuture(ClosedSessionException.get());
        }

        try {
            final ByteBuf buf = toByteBuf(ctx, data);
            final HttpContent content;
            if (endStream) {
                content = new DefaultLastHttpContent(buf);
            } else {
                content = new DefaultHttpContent(buf);
            }
            return write(ctx, id, content, endStream);
        } catch (Throwable t) {
            return ctx.newFailedFuture(t);
        }
    }

    private ChannelFuture write(ChannelHandlerContext ctx, int id, HttpObject obj, boolean endStream) {
        if (id < currentId) {
            // Attempted to write something on a finished request/response; discard.
            // e.g. the request already timed out.
            return ctx.newFailedFuture(ClosedPublisherException.get());
        }

        final PendingWrites currentPendingWrites = pendingWrites.get(id);
        if (id == currentId) {
            if (currentPendingWrites != null) {
                pendingWrites.remove(id);
                flushPendingWrites(ctx, currentPendingWrites);
            }

            final ChannelFuture future = ctx.write(obj);
            if (endStream) {
                currentId++;

                // The next PendingWrites might be complete already.
                for (;;) {
                    final PendingWrites nextPendingWrites = pendingWrites.get(currentId);
                    if (nextPendingWrites == null) {
                        break;
                    }

                    flushPendingWrites(ctx, nextPendingWrites);
                    if (!nextPendingWrites.isEndOfStream()) {
                        break;
                    }

                    pendingWrites.remove(currentId);
                    currentId++;
                }
            }

            ctx.flush();
            return future;
        } else {
            final ChannelPromise promise = ctx.newPromise();
            final Entry<HttpObject, ChannelPromise> entry = new SimpleImmutableEntry<>(obj, promise);

            if (currentPendingWrites == null) {
                final PendingWrites newPendingWrites = new PendingWrites();
                maxIdWithPendingWrites = Math.max(maxIdWithPendingWrites, id);
                newPendingWrites.add(entry);
                pendingWrites.put(id, newPendingWrites);
            } else {
                currentPendingWrites.add(entry);
            }

            if (endStream) {
                currentPendingWrites.setEndOfStream();
            }

            return promise;
        }
    }

    private static void flushPendingWrites(ChannelHandlerContext ctx, PendingWrites pendingWrites) {
        for (;;) {
            final Entry<HttpObject, ChannelPromise> e = pendingWrites.poll();
            if (e == null) {
                break;
            }

            ctx.write(e.getKey(), e.getValue());
        }
    }

    @Override
    protected ChannelFuture doWriteReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) {
        // NB: this.minClosedId can be overwritten more than once when 3+ pipelined requests are received
        //     and they are handled by different threads simultaneously.
        //     e.g. when the 3rd request triggers a reset and then the 2nd one triggers another.
        minClosedId = Math.min(minClosedId, id);
        for (int i = minClosedId; i <= maxIdWithPendingWrites; i++) {
            final PendingWrites pendingWrites = this.pendingWrites.remove(i);
            for (;;) {
                final Entry<HttpObject, ChannelPromise> e = pendingWrites.poll();
                if (e == null) {
                    break;
                }
                e.getValue().tryFailure(ClosedSessionException.get());
            }
        }

        final ChannelFuture f = ctx.write(Unpooled.EMPTY_BUFFER);
        if (currentId >= minClosedId) {
            f.addListener(ChannelFutureListener.CLOSE);
        }

        return f;
    }

    @Override
    protected void doClose() {
        if (pendingWrites.isEmpty()) {
            return;
        }

        final ClosedSessionException cause = ClosedSessionException.get();
        for (Queue<Entry<HttpObject, ChannelPromise>> queue : pendingWrites.values()) {
            for (;;) {
                final Entry<HttpObject, ChannelPromise> e = queue.poll();
                if (e == null) {
                    break;
                }

                e.getValue().tryFailure(cause);
            }
        }

        pendingWrites.clear();
    }

    private static final class PendingWrites extends ArrayDeque<Entry<HttpObject, ChannelPromise>> {

        private static final long serialVersionUID = 4241891747461017445L;

        private boolean endOfStream;

        PendingWrites() {
            super(4);
        }

        boolean isEndOfStream() {
            return endOfStream;
        }

        void setEndOfStream() {
            endOfStream = true;
        }
    }
}