org.jooby.internal.netty.NettyRequest.java Source code

Java tutorial

Introduction

Here is the source code for org.jooby.internal.netty.NettyRequest.java

Source

/**
 * 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.
 */
package org.jooby.internal.netty;

import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.jooby.MediaType;
import org.jooby.Sse;
import org.jooby.spi.NativePushPromise;
import org.jooby.spi.NativeRequest;
import org.jooby.spi.NativeUpload;
import org.jooby.spi.NativeWebSocket;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpData;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.util.AttributeKey;

public class NettyRequest implements NativeRequest {

    public static final AttributeKey<String> PROTOCOL = AttributeKey
            .newInstance(NettyRequest.class.getName() + ".protol");

    public static final AttributeKey<Boolean> NEED_FLUSH = AttributeKey
            .newInstance(NettyRequest.class.getName() + ".needFlush");

    public static final AttributeKey<Boolean> ASYNC = AttributeKey
            .newInstance(NettyRequest.class.getName() + ".async");

    public static final AttributeKey<Boolean> SECURE = AttributeKey
            .newInstance(NettyRequest.class.getName() + ".secure");;

    private HttpRequest req;

    private QueryStringDecoder query;

    private List<org.jooby.Cookie> cookies;

    private Multimap<String, String> params;

    private Multimap<String, NativeUpload> files;

    private String tmpdir;

    private String path;

    private ChannelHandlerContext ctx;

    private int wsMaxMessageSize;

    public NettyRequest(final ChannelHandlerContext ctx, final HttpRequest req, final String tmpdir,
            final int wsMaxMessageSize) throws IOException {
        this.ctx = ctx;
        this.req = req;
        this.tmpdir = tmpdir;
        this.query = new QueryStringDecoder(req.uri());
        this.path = URLDecoder.decode(query.path(), "UTF-8");
        this.wsMaxMessageSize = wsMaxMessageSize;
        Channel channel = ctx.channel();
        channel.attr(ASYNC).set(false);
    }

    @Override
    public Optional<String> queryString() {
        String uri = req.uri();
        int at = uri.indexOf('?') + 1;
        return at > 0 && at < uri.length() ? Optional.of(uri.substring(at)) : Optional.empty();
    }

    @Override
    public String method() {
        return req.method().name();
    }

    @Override
    public String rawPath() {
        String uri = req.uri();
        int at = uri.indexOf('?');
        return at > 0 ? uri.substring(0, at) : uri;
    }

    @Override
    public String path() {
        return path;
    }

    @Override
    public List<String> paramNames() throws IOException {
        return ImmutableList.copyOf(decodeParams().keySet());
    }

    @Override
    public List<String> params(final String name) throws Exception {
        return (List<String>) decodeParams().get(name);
    }

    @Override
    public List<String> headers(final String name) {
        return req.headers().getAll(name);
    }

    @Override
    public Optional<String> header(final String name) {
        String value = req.headers().get(name);
        return Optional.ofNullable(value);
    }

    @Override
    public List<String> headerNames() {
        ImmutableList.Builder<String> builder = ImmutableList.builder();
        req.headers().names().forEach(it -> builder.add(it.toString()));
        return builder.build();
    }

    @Override
    public List<org.jooby.Cookie> cookies() {
        if (this.cookies == null) {
            String cookieString = req.headers().get(HttpHeaderNames.COOKIE);
            if (cookieString != null) {
                this.cookies = ServerCookieDecoder.STRICT.decode(cookieString).stream().map(this::cookie)
                        .collect(Collectors.toList());

            } else {
                this.cookies = Collections.emptyList();
            }
        }
        return this.cookies;
    }

    @Override
    public List<NativeUpload> files(final String name) throws IOException {
        decodeParams();
        return ImmutableList.copyOf(this.files.get(name));
    }

    @Override
    public InputStream in() throws IOException {
        ByteBuf content = ((HttpContent) req).content();
        return new ByteBufInputStream(content);
    }

    @Override
    public String ip() {
        InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress();
        return remoteAddress.getAddress().getHostAddress();
    }

    @Override
    public String protocol() {
        return ctx.pipeline().get("h2") == null ? req.protocolVersion().text() : "HTTP/2.0";
    }

    @Override
    public boolean secure() {
        return ifSecure(Boolean.TRUE, Boolean.FALSE).booleanValue();
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T upgrade(final Class<T> type) throws Exception {
        if (type == NativeWebSocket.class) {
            String protocol = ifSecure("wss", "ws");
            String webSocketURL = protocol + "://" + req.headers().get(HttpHeaderNames.HOST) + path;

            WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(webSocketURL, null,
                    true, wsMaxMessageSize);
            WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
            NettyWebSocket result = new NettyWebSocket(ctx, handshaker, (ws) -> {
                handshaker.handshake(ctx.channel(), (FullHttpRequest) req).addListener(FIRE_EXCEPTION_ON_FAILURE)
                        .addListener(payload -> ws.connect()).addListener(FIRE_EXCEPTION_ON_FAILURE);
            });
            ctx.channel().attr(NettyWebSocket.KEY).set(result);
            return (T) result;
        } else if (type == Sse.class) {
            NettySse sse = new NettySse(ctx);
            return (T) sse;
        } else if (type == NativePushPromise.class) {
            return (T) new NettyPush(ctx,
                    req.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()),
                    header("host").orElse(ip()), ifSecure("https", "http"));
        }
        throw new UnsupportedOperationException("Not Supported: " + type);
    }

    @Override
    public void startAsync(final Executor executor, final Runnable runnable) {
        Channel channel = ctx.channel();
        channel.attr(NEED_FLUSH).set(false);
        channel.attr(ASYNC).set(true);

        executor.execute(runnable);
    }

    private org.jooby.Cookie cookie(final Cookie c) {
        org.jooby.Cookie.Definition cookie = new org.jooby.Cookie.Definition(c.name(), c.value());
        Optional.ofNullable(c.domain()).ifPresent(cookie::domain);
        Optional.ofNullable(c.path()).ifPresent(cookie::path);

        return cookie.toCookie();
    }

    private Multimap<String, String> decodeParams() throws IOException {
        if (params == null) {
            params = ArrayListMultimap.create();
            files = ArrayListMultimap.create();

            query.parameters().forEach((name, values) -> values.forEach(value -> params.put(name, value)));

            HttpMethod method = req.method();
            boolean hasBody = method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT)
                    || method.equals(HttpMethod.PATCH);
            boolean formLike = false;
            if (req.headers().contains("Content-Type")) {
                String contentType = req.headers().get("Content-Type").toLowerCase();
                formLike = (contentType.startsWith(MediaType.multipart.name())
                        || contentType.startsWith(MediaType.form.name()));
            }
            if (hasBody && formLike) {
                HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(), req);
                try {
                    Function<HttpPostRequestDecoder, Boolean> hasNext = it -> {
                        try {
                            return it.hasNext();
                        } catch (HttpPostRequestDecoder.EndOfDataDecoderException ex) {
                            return false;
                        }
                    };
                    while (hasNext.apply(decoder)) {
                        HttpData field = (HttpData) decoder.next();
                        try {
                            String name = field.getName();
                            if (field.getHttpDataType() == HttpDataType.FileUpload) {
                                files.put(name, new NettyUpload((FileUpload) field, tmpdir));
                            } else {
                                params.put(name, field.getString());
                            }
                        } finally {
                            field.release();
                        }
                    }
                } finally {
                    decoder.destroy();
                }
            }
        }
        return params;
    }

    private <T> T ifSecure(final T then, final T otherwise) {
        return ctx.pipeline().get("ssl") != null ? then : otherwise;
    }
}