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

io.micronaut.http.netty.NettyMutableHttpResponse Maven / Gradle / Ivy

There is a newer version: 4.5.3
Show newest version
/*
 * Copyright 2017-2020 original authors
 *
 * 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
 *
 * https://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 io.micronaut.http.netty;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.TypeHint;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.value.MutableConvertibleValues;
import io.micronaut.core.convert.value.MutableConvertibleValuesMap;
import io.micronaut.core.io.buffer.ByteBuffer;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpHeaders;
import io.micronaut.http.MutableHttpMessage;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.body.MessageBodyWriter;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.http.netty.cookies.NettyCookie;
import io.micronaut.http.netty.stream.DefaultStreamedHttpResponse;
import io.micronaut.http.netty.stream.StreamedHttpResponse;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;

import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Delegates to Netty's {@link FullHttpResponse}.
 *
 * @param  The response body
 * @author Graeme Rocher
 * @since 1.0
 */
@Internal
@TypeHint(value = NettyMutableHttpResponse.class)
public final class NettyMutableHttpResponse implements MutableHttpResponse, NettyHttpResponseBuilder {
    private static final ServerCookieEncoder DEFAULT_SERVER_COOKIE_ENCODER = ServerCookieEncoder.LAX;

    private final HttpVersion httpVersion;
    private HttpResponseStatus httpResponseStatus;
    private final NettyHttpHeaders headers;
    private Object body;
    private Optional optionalBody;
    private final HttpHeaders nettyHeaders;
    private final HttpHeaders trailingNettyHeaders;
    private final DecoderResult decoderResult;
    private final ConversionService conversionService;
    private MutableConvertibleValues attributes;
    private ServerCookieEncoder serverCookieEncoder = DEFAULT_SERVER_COOKIE_ENCODER;

    private final BodyConvertor bodyConvertor = newBodyConvertor();
    private MessageBodyWriter messageBodyWriter;

    /**
     * @param nettyResponse     The {@link FullHttpResponse}
     * @param conversionService The conversion service
     */
    public NettyMutableHttpResponse(FullHttpResponse nettyResponse, ConversionService conversionService) {
        this(nettyResponse.protocolVersion(), nettyResponse.status(), nettyResponse.headers(), nettyResponse.trailingHeaders(), nettyResponse.content(), nettyResponse.decoderResult(), conversionService);
    }

    /**
     * @param conversionService The conversion service
     */
    public NettyMutableHttpResponse(ConversionService conversionService) {
        this(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, null, conversionService);
    }

    /**
     * Creates a new instance.
     *
     * @param httpVersion The http version
     * @param httpResponseStatus THe http response status
     * @param conversionService The conversion service
     */
    public NettyMutableHttpResponse(HttpVersion httpVersion, HttpResponseStatus httpResponseStatus, ConversionService conversionService) {
        this(httpVersion, httpResponseStatus, null, conversionService);
    }

    /**
     * Creates a new instance.
     *
     * @param httpVersion The http version
     * @param httpResponseStatus THe http response status
     * @param body The body
     * @param conversionService The conversion service
     */
    public NettyMutableHttpResponse(HttpVersion httpVersion, HttpResponseStatus httpResponseStatus, Object body, ConversionService conversionService) {
        this(httpVersion, httpResponseStatus, null, body, conversionService);
    }

    /**
     * Creates a new instance.
     *
     * @param httpVersion The http version
     * @param httpResponseStatus THe http response status
     * @param nettyHeaders The http headers
     * @param body The body
     * @param conversionService The conversion service
     */
    public NettyMutableHttpResponse(HttpVersion httpVersion,
                                    HttpResponseStatus httpResponseStatus,
                                    HttpHeaders nettyHeaders,
                                    Object body,
                                    ConversionService conversionService) {
        this(httpVersion, httpResponseStatus, nettyHeaders, EmptyHttpHeaders.INSTANCE, body, null, conversionService);
    }

    private NettyMutableHttpResponse(HttpVersion httpVersion,
                                     HttpResponseStatus httpResponseStatus,
                                     HttpHeaders nettyHeaders,
                                     HttpHeaders trailingNettyHeaders,
                                     Object body,
                                     DecoderResult decoderResult,
                                     ConversionService conversionService) {
        this.httpVersion = httpVersion;
        this.httpResponseStatus = httpResponseStatus;
        this.trailingNettyHeaders = trailingNettyHeaders;
        this.decoderResult = decoderResult;
        this.conversionService = conversionService;

        boolean hasHeaders = nettyHeaders != null;
        if (!hasHeaders) {
            nettyHeaders = new DefaultHttpHeaders(false);
        }
        this.nettyHeaders = nettyHeaders;
        this.headers = new NettyHttpHeaders(nettyHeaders, conversionService);
        if (body == null) {
            this.body = null;
            this.optionalBody = Optional.empty();
        } else {
            this.body = body;
            this.optionalBody = Optional.of(body);
            Optional mediaType = MediaType.fromType(body.getClass());
            if (mediaType.isPresent() && (!hasHeaders || !nettyHeaders.contains(HttpHeaderNames.CONTENT_TYPE))) {
                contentType(mediaType.get());
            }
        }
    }

    @Override
    public Optional> getBodyWriter() {
        return Optional.ofNullable(messageBodyWriter);
    }

    @Override
    public MutableHttpMessage bodyWriter(MessageBodyWriter messageBodyWriter) {
        this.messageBodyWriter = messageBodyWriter;
        return this;
    }

    /**
     * The netty http version.
     *
     * @return http version
     */
    public HttpVersion getNettyHttpVersion() {
        return httpVersion;
    }

    /**
     * The netty http response status.
     *
     * @return http response status
     */
    public HttpResponseStatus getNettyHttpStatus() {
        return httpResponseStatus;
    }

    /**
     * The netty headers.
     *
     * @return netty headers
     */
    public HttpHeaders getNettyHeaders() {
        return nettyHeaders;
    }

    @Override
    public String toString() {
        return code() + " " + reason();
    }

    @Override
    public MutableHttpHeaders getHeaders() {
        return headers;
    }

    @Override
    public MutableConvertibleValues getAttributes() {
        MutableConvertibleValues attributes = this.attributes;
        if (attributes == null) {
            synchronized (this) { // double check
                attributes = this.attributes;
                if (attributes == null) {
                    attributes = new MutableConvertibleValuesMap<>(new ConcurrentHashMap<>(4));
                    this.attributes = attributes;
                }
            }
        }
        return attributes;
    }

    @Override
    public io.micronaut.http.HttpResponse setAttribute(CharSequence name, Object value) {
        // This is the copy from the super method to avoid the type pollution
        if (StringUtils.isNotEmpty(name)) {
            if (value == null) {
                getAttributes().remove(name.toString());
            } else {
                getAttributes().put(name.toString(), value);
            }
        }
        return this;
    }

    @Override
    public int code() {
        return httpResponseStatus.code();
    }

    @Override
    public String reason() {
        return httpResponseStatus.reasonPhrase();
    }

    @Override
    public MutableHttpResponse cookie(Cookie cookie) {
        if (cookie instanceof NettyCookie) {
            NettyCookie nettyCookie = (NettyCookie) cookie;
            String value = serverCookieEncoder.encode(nettyCookie.getNettyCookie());
            headers.add(HttpHeaderNames.SET_COOKIE, value);
        } else {
            throw new IllegalArgumentException("Argument is not a Netty compatible Cookie");
        }
        return this;
    }

    @Override
    public MutableHttpResponse cookies(Set cookies) {
        if (cookies == null || cookies.isEmpty()) {
            return this;
        }
        for (Cookie cookie : cookies) {
            cookie(cookie);
        }
        return this;
    }

    @Override
    public Optional getBody() {
        return (Optional) optionalBody;
    }

    @Override
    public  Optional getBody(Class type) {
        return getBody(Argument.of(type));
    }

    @Override
    public  Optional getBody(ArgumentConversionContext conversionContext) {
        return bodyConvertor.convert(conversionContext, body);
    }

    @Override
    public MutableHttpResponse status(int status, CharSequence message) {
        if (message == null) {
            message = HttpStatus.getDefaultReason(status);
        }
        httpResponseStatus = new HttpResponseStatus(status, message.toString());
        return this;
    }

    @Override
    public  MutableHttpResponse body(@Nullable T body) {
        if (this.body != body) {
            if (this.body instanceof ByteBuf) {
                ((ByteBuf) this.body).release();
            }
            setBody(body);
            bodyConvertor.cleanup();
        }
        return (MutableHttpResponse) this;
    }

    @Override
    public MutableHttpResponse contentType(MediaType mediaType) {
        if (mediaType == null) {
            headers.remove(HttpHeaderNames.CONTENT_TYPE);
        } else {
            // optimization for content type validation
            mediaType.validate(() -> NettyHttpHeaders.validateHeader(HttpHeaderNames.CONTENT_TYPE, mediaType));
            headers.setUnsafe(HttpHeaderNames.CONTENT_TYPE, mediaType);
        }
        return this;
    }

    /**
     * @return Server cookie encoder
     */
    public ServerCookieEncoder getServerCookieEncoder() {
        return serverCookieEncoder;
    }

    /**
     * @param serverCookieEncoder Server cookie encoder
     */
    public void setServerCookieEncoder(ServerCookieEncoder serverCookieEncoder) {
        this.serverCookieEncoder = serverCookieEncoder;
    }

    @NonNull
    @Override
    public FullHttpResponse toFullHttpResponse() {
        ByteBuf content;
        if (body == null) {
            content = Unpooled.EMPTY_BUFFER;
        } else if (body instanceof ByteBuf) {
            content = (ByteBuf) body;
        } else {
            throw new IllegalStateException("Body needs to be converted to ByteBuf from " + body.getClass());
        }
        DefaultFullHttpResponse defaultFullHttpResponse = new DefaultFullHttpResponse(httpVersion,
                httpResponseStatus,
                content,
                nettyHeaders,
                trailingNettyHeaders);
        if (decoderResult != null) {
            defaultFullHttpResponse.setDecoderResult(decoderResult);
        }
        return defaultFullHttpResponse;
    }

    @NonNull
    @Override
    public StreamedHttpResponse toStreamHttpResponse() {
        ByteBuf content;
        if (body == null) {
            content = Unpooled.EMPTY_BUFFER;
        } else if (body instanceof ByteBuf) {
            content = (ByteBuf) body;
        } else {
            throw new IllegalStateException("Body needs to be converted to ByteBuf from " + body.getClass());
        }
        DefaultStreamedHttpResponse streamedHttpResponse = new DefaultStreamedHttpResponse(
                HttpVersion.HTTP_1_1,
                httpResponseStatus,
                true,
                Publishers.just(new DefaultLastHttpContent(content))
        );
        streamedHttpResponse.headers().setAll(nettyHeaders);
        return streamedHttpResponse;
    }

    @NonNull
    @Override
    public HttpResponse toHttpResponse() {
        return toFullHttpResponse();
    }

    @Override
    public boolean isStream() {
        return false;
    }

    private void setBody(Object body) {
        this.body = body;
        this.optionalBody = Optional.ofNullable(body);
        Optional contentType = getContentType();
        if (!contentType.isPresent() && body != null) {
            MediaType.fromType(body.getClass()).ifPresent(this::contentType);
        }
    }

    private BodyConvertor newBodyConvertor() {
        return new BodyConvertor() {

            @Override
            public Optional convert(ArgumentConversionContext conversionContext, Object value) {
                if (value == null) {
                    return Optional.empty();
                }
                if (Argument.OBJECT_ARGUMENT.equalsType(conversionContext.getArgument())) {
                    return Optional.of(value);
                }
                return convertFromNext(conversionService, conversionContext, value);
            }

        };
    }

    private abstract static class BodyConvertor {

        private BodyConvertor nextConvertor;

        public abstract Optional convert(ArgumentConversionContext valueType, T value);

        protected synchronized Optional convertFromNext(ConversionService conversionService, ArgumentConversionContext conversionContext, T value) {
            if (nextConvertor == null) {
                Optional conversion;
                if (value instanceof ByteBuffer) {
                    conversion = conversionService.convert(((ByteBuffer) value).asNativeBuffer(), conversionContext);
                } else {
                    conversion = conversionService.convert(value, conversionContext);
                }
                nextConvertor = new BodyConvertor() {

                    @Override
                    public Optional convert(ArgumentConversionContext currentConversionContext, T value) {
                        if (currentConversionContext == conversionContext) {
                            return conversion;
                        }
                        if (currentConversionContext.getArgument().equalsType(conversionContext.getArgument())) {
                            conversionContext.getLastError().ifPresent(error -> {
                                error.getOriginalValue().ifPresentOrElse(
                                    originalValue -> currentConversionContext.reject(originalValue, error.getCause()),
                                    () -> currentConversionContext.reject(error.getCause())
                                );
                            });
                            return conversion;
                        }
                        return convertFromNext(conversionService, currentConversionContext, value);
                    }

                };
                return conversion;
            }
            return nextConvertor.convert(conversionContext, value);
        }

        public void cleanup() {
            nextConvertor = null;
        }

    }

}