com.github.yongchristophertang.engine.web.request.RequestProxy.java Source code

Java tutorial

Introduction

Here is the source code for com.github.yongchristophertang.engine.web.request.RequestProxy.java

Source

/*
 * Copyright 2014-2015 the original author or 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
 *
 *       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.github.yongchristophertang.engine.web.request;

import com.github.yongchristophertang.engine.web.annotations.*;
import com.github.yongchristophertang.engine.web.http.HttpMethod;
import com.github.yongchristophertang.engine.web.http.MultipartBodyFormBuilder;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import javaslang.control.Try;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;

import static com.github.yongchristophertang.engine.AssertUtils.notNull;

/**
 * Request proxy singleton, which implements {@link java.lang.reflect.InvocationHandler} to handle forwarded method
 * callings from the proxy class.
 * The class has no mutable states and can be viewed as a pure function, thus making it effectively thread safe.
 * Singleton pattern will not cause any troubles.
 *
 * @author Yong Tang
 * @since 0.4
 */
public final class RequestProxy implements InvocationHandler {
    private static final RequestProxy INSTANCE = new RequestProxy();
    private static final Logger LOGGER = LogManager.getLogger();
    /**
     * Use the reflected constructor to initialize a {@link java.lang.invoke.MethodHandles.Lookup} in order to
     * disable the access check with method {@link java.lang.invoke.MethodHandles.Lookup#in} which prevents access to
     * "default" methods if the caller class, {@link RequestProxy} in this
     * case, and the {@code requestLookupClass} do not reside in the same package.
     */
    private static final Constructor<MethodHandles.Lookup> LOOKUP_CONSTRUCTOR;

    static {
        try {
            LOOKUP_CONSTRUCTOR = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
            if (!LOOKUP_CONSTRUCTOR.isAccessible()) {
                LOOKUP_CONSTRUCTOR.setAccessible(true);
            }
        } catch (NoSuchMethodException exp) {
            throw new IllegalStateException(exp);
        }
    }

    /**
     * Disable direct initialization
     */
    private RequestProxy() {
    }

    /**
     * Get the singleton instance of {@link RequestProxy}.
     */
    public static RequestProxy getInstance() {
        return INSTANCE;
    }

    // TODO: too messy, need to refactor data collection part
    @Override
    @SuppressWarnings("unchecked")
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // if the method is a default method, process with the default implementation
        if (method.isDefault()) {
            return LOOKUP_CONSTRUCTOR.newInstance(method.getDeclaringClass(), MethodHandles.Lookup.PRIVATE)
                    .unreflectSpecial(method, method.getDeclaringClass()).bindTo(proxy).invokeWithArguments(args);
        }

        Multimap<String, String> queryParams = ArrayListMultimap.create();
        Multimap<String, String> bodyParams = ArrayListMultimap.create();
        Map<String, String> pathParams = new HashMap<>();
        Multimap<String, String> fileParams = ArrayListMultimap.create();
        Map<String, String> headerParams = new HashMap<>();

        /* Handle the class level annotations: Host and Path */
        // The url will be the URL path for the request
        String url = getHost(method.getDeclaringClass().getAnnotation(Host.class));

        /* Handle the method level annotations: Path, HTTPMethod, Produce and Consume */
        url += getPath(method.getDeclaringClass().getAnnotation(Path.class))
                + getPath(method.getAnnotation(Path.class));

        /* Fill out the api's description */
        String description = Optional.ofNullable(method.getAnnotation(Description.class)).map(Description::value)
                .orElse(method.getName());

        // Handle Produce and Consume
        String contentType = Optional.ofNullable(method.getAnnotation(Produce.class)).map(Produce::value)
                .orElse(null);
        String accept = Optional.ofNullable(method.getAnnotation(Consume.class)).map(Consume::value).orElse(null);

        // Handle HTTPMethod, only ONE HTTPMethod can be annotated with one method
        List<HTTPMethod> httpMethods = Arrays.asList(method.getAnnotations()).stream()
                .map(a -> a.annotationType().getAnnotation(HTTPMethod.class)).filter(m -> m != null)
                .collect(Collectors.toList());
        if (httpMethods.size() != 1) {
            throw new IllegalArgumentException("HTTPMethod annotation must be annotated once, no more and no less");
        }

        notNull(httpMethods.get(0).value(), "Http Method is not defined");
        HttpMethod httpMethod = HttpMethod.valueOf(httpMethods.get(0).value());

        /*
        Handle the field level annotations: PathParam, BodyParam, QueryParam and HeaderParam.
        Noted that only the first annotation will be processed.
         */
        Field[] fields = method.getDeclaringClass().getFields();
        try {
            for (Field f : fields) {
                Object value = f.get(null);
                if (value != null) {
                    Class<? extends Annotation> annotationType = f.getAnnotations()[0].annotationType();
                    if (annotationType == PathParam.class) {
                        PathParam a = f.getAnnotation(PathParam.class);
                        pathParams.put(a.value(), a.converter().newInstance().convert(value));
                    } else if (annotationType == BodyParam.class) {
                        BodyParam a = f.getAnnotation(BodyParam.class);
                        if (value instanceof Collection) {
                            bodyParams.putAll(a.value(),
                                    (Iterable<? extends String>) ((Collection) value).stream().map(
                                            v -> Try.of(() -> a.converter().newInstance().convert(v)).orElse(""))
                                            .collect(Collectors.toList()));
                        } else if (value instanceof Map) {
                            ((Map<String, Object>) value).entrySet().parallelStream()
                                    .forEach(e -> bodyParams.put(e.getKey(), e.getValue().toString()));
                        } else {
                            bodyParams.put(a.value(), a.converter().newInstance().convert(value));
                        }
                    } else if (annotationType == QueryParam.class) {
                        QueryParam a = f.getAnnotation(QueryParam.class);
                        if (value instanceof Collection) {
                            queryParams.putAll(a.value(),
                                    (Iterable<? extends String>) ((Collection) value).stream().map(
                                            v -> Try.of(() -> a.converter().newInstance().convert(v)).orElse(""))
                                            .collect(Collectors.toList()));
                        } else {
                            queryParams.put(a.value(), a.converter().newInstance().convert(value));
                        }
                    } else if (annotationType == HeaderParam.class) {
                        HeaderParam a = f.getAnnotation(HeaderParam.class);
                        headerParams.put(a.value(), a.converter().newInstance().convert(value));
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.error("Cannot process filed level annotations");
        }
        //        Stream.of(method.getDeclaringClass().getFields()).filter(f -> Try.of(() -> Optional.of(f.get(null)).isPresent()).orElse(false)).forEach(f ->
        //                Optional.ofNullable(f.getAnnotations()[0]).map(Match.as(String.class)
        //                        .whenType(PathParam.class).then(anno -> Try.of(() -> anno.converter().newInstance().convert(f.get(null)))
        //                            .onSuccess(p -> pathParams.put(anno.value(), p)).orElse(""))
        //                        .whenType(BodyParam.class).then(anno -> Try.of(() -> {
        //                                Object object = f.get(null);
        //                                if (object instanceof Collection) {
        //                                    return ((Collection) object).stream().map(e -> Try.of(() -> anno.converter().newInstance().convert(e)).orElse("")).collect(Collectors.toList());
        //                                } else {
        //                                    return anno.converter().newInstance().convert(object);
        //                                }
        //                            }).onSuccess(p -> {
        //                                if (p instanceof Collection) {
        //                                    bodyParams.putAll(anno.value(), (Iterable<String>) p);
        //                                } else {
        //                                    bodyParams.put(anno.value(), String.valueOf(p));
        //                                }
        //                            }).orElse(""))
        //                        .whenType(QueryParam.class).then(anno -> Try.of(() -> anno.converter().newInstance().convert(f.get(null)))
        //                            .onSuccess(p -> queryParams.put(anno.value(), p)).orElse(""))
        //                        .whenType(HeaderParam.class).then(anno -> Try.of(() -> anno.converter().newInstance().convert(f.get(null)))
        //                            .onSuccess(p -> headerParams.put(anno.value(), p)).orElse(""))));

        /*
        Handle the method parameter level annotations: PathParam, BodyParam, QueryParam, HeaderParam and FileParam.
        Noted that only the first annotation will be processed.
        */
        Parameter[] parameters = method.getParameters();
        try {
            for (int i = 0; i < args.length; i++) {
                if (args[i] != null && parameters[i].getAnnotations() != null) {
                    Class<? extends Annotation> annotationType = parameters[i].getAnnotations()[0].annotationType();
                    if (annotationType == PathParam.class) {
                        PathParam a = parameters[i].getAnnotation(PathParam.class);
                        pathParams.put(a.value(), a.converter().newInstance().convert(args[i]));
                    } else if (annotationType == BodyParam.class) {
                        BodyParam a = parameters[i].getAnnotation(BodyParam.class);
                        if (args[i] instanceof Collection) {
                            bodyParams.putAll(a.value(),
                                    (Iterable<? extends String>) ((Collection) args[i]).stream().map(
                                            v -> Try.of(() -> a.converter().newInstance().convert(v)).orElse(""))
                                            .collect(Collectors.toList()));
                        } else if (args[i] instanceof Map) {
                            ((Map<String, Object>) args[i]).entrySet().parallelStream()
                                    .forEach(e -> bodyParams.put(e.getKey(), e.getValue().toString()));
                        } else {
                            bodyParams.put(a.value(), a.converter().newInstance().convert(args[i]));
                        }
                    } else if (annotationType == QueryParam.class) {
                        QueryParam a = parameters[i].getAnnotation(QueryParam.class);
                        if (args[i] instanceof Collection) {
                            queryParams.putAll(a.value(),
                                    (Iterable<? extends String>) ((Collection) args[i]).stream().map(
                                            v -> Try.of(() -> a.converter().newInstance().convert(v)).orElse(""))
                                            .collect(Collectors.toList()));
                        } else if (args[i] instanceof Map) {
                            ((Map<String, Object>) args[i]).entrySet().parallelStream()
                                    .forEach(e -> queryParams.put(e.getKey(), e.getValue().toString()));
                        } else {
                            queryParams.put(a.value(), a.converter().newInstance().convert(args[i]));
                        }
                    } else if (annotationType == HeaderParam.class) {
                        HeaderParam a = parameters[i].getAnnotation(HeaderParam.class);
                        headerParams.put(a.value(), a.converter().newInstance().convert(args[i]));
                    } else if (annotationType == FileParam.class) {
                        FileParam a = parameters[i].getAnnotation(FileParam.class);
                        fileParams.put(a.value(), args[i].toString());
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.error("Cannot process parameter level annotations");
        }

        //        Stream.range(0, parameters.length).filter(
        //            i -> args[i] != null & parameters[i].getAnnotations() != null).map(
        //            i -> new Tuple2<>(parameters[i].getAnnotations()[0], args[i])).forEach(t -> Option.of(t._1).map(Match.as(String.class)
        //                .whenType(PathParam.class).then(p -> Try.of(() -> p.converter().newInstance().convert(t._2))
        //                    .onSuccess(v -> pathParams.put(p.value(), v)).orElse(""))
        //                .whenType(BodyParam.class).then(p -> Try.of(() -> p.converter().newInstance().convert(t._2))
        //                    .onSuccess(v -> bodyParams.put(p.value(), v)).orElse(""))
        //                .whenType(QueryParam.class).then(p -> Try.of(() -> p.converter().newInstance().convert(t._2))
        //                    .onSuccess(v -> queryParams.put(p.value(), v)).orElse(""))
        //                .whenType(HeaderParam.class).then(p -> Try.of(() -> p.converter().newInstance().convert(t._2))
        //                    .onSuccess(v -> headerParams.put(p.value(), v)).orElse(""))
        //                .whenType(FileParam.class).then(p -> String.valueOf(fileParams.put(p.value(), t._2.toString()))
        //            )));

        /**
         * Local inner class for creating a {@link HttpRequestBuilders} or {@link HttpMultipartRequestBuilders}.
         * This class provides a {@code build} method to construct the target {@code builders}.
         */
        abstract class RequestBuilderConstructor {
            HttpRequestBuilders builders;

            /**
             * Build path parameters.
             */
            void pathBuild() {
                pathParams.keySet().stream().forEach(key -> builders.path(key, pathParams.get(key)));
            }

            /**
             * Build query string parameters.
             */
            void queryBuild() {
                queryParams.keySet().stream().forEach(key -> builders.param(key, queryParams.get(key)));
            }

            /**
             * Build ordinary header parameters.
             */
            void headerBuild() {
                headerParams.keySet().stream().forEach(key -> builders.header(key, headerParams.get(key)));
            }

            /**
             * Build a special header parameter, ContentType.
             * The method behaviour varies with different request types..
             */
            abstract void contentTypeBuild();

            /**
             * Build a special header parameter, Accept.
             */
            void acceptBuild() {
                Optional.ofNullable(accept).ifPresent(builders::accept);
            }

            /**
             * Build body form parameters.
             * The method behaviour varies with different request types.
             */
            abstract void bodyBuild();

            /**
             * Attach files with requests.
             * This method is only valid for multipart request type.
             */
            abstract void fileBuild();

            /**
             * Post processors for returning {@code builders}
             */
            abstract void postProcessor();

            /**
             * Return a {@link HttpRequestBuilders}.
             */
            final HttpRequestBuilders build() {
                pathBuild();
                queryBuild();
                headerBuild();
                contentTypeBuild();
                acceptBuild();
                bodyBuild();
                fileBuild();
                postProcessor();
                return builders;
            }
        }

        /**
         * Local inner class for creating a {@link HttpMultipartRequestBuilders}.
         */
        class MultipartConstructor extends RequestBuilderConstructor {
            MultipartBodyFormBuilder multipartBodyFormBuilder = MultipartBodyFormBuilder.create();

            MultipartConstructor(HttpMultipartRequestBuilders builders) {
                this.builders = builders;
            }

            @Override
            void contentTypeBuild() {

            }

            @Override
            void bodyBuild() {
                bodyParams.keySet().stream()
                        .forEach(key -> multipartBodyFormBuilder.param(key, bodyParams.get(key)));
            }

            @Override
            void fileBuild() {
                fileParams.keySet().stream().forEach(key -> fileParams.get(key).stream().forEach(value -> {
                    if (value.contains(",")) {
                        for (String file : value.split(",")) {
                            multipartBodyFormBuilder.file(key, file);
                        }
                    } else if (!value.isEmpty()) {
                        multipartBodyFormBuilder.file(key, value);
                    }
                }));
            }

            @Override
            void postProcessor() {
                HttpMultipartRequestBuilders.class.cast(builders).body(multipartBodyFormBuilder);
            }
        }

        /**
         * Local inner class for creating a {@link HttpRequestBuilders}.
         */
        class RequestConstructor extends RequestBuilderConstructor {

            RequestConstructor(HttpRequestBuilders builders) {
                this.builders = builders;
            }

            @Override
            void contentTypeBuild() {
                Optional.ofNullable(contentType).ifPresent(builders::contentType);
            }

            @Override
            void bodyBuild() {
                bodyParams.keySet().stream().forEach(key -> {
                    if (key.equals("")) {
                        // if use a raw string to set up body, then we only add the first value, igonor the further
                        builders.body(bodyParams.get(key).iterator().next());
                    } else {
                        builders.body(key, bodyParams.get(key));
                    }
                });
            }

            @Override
            void fileBuild() {

            }

            @Override
            void postProcessor() {

            }
        }

        return fileParams.isEmpty()
                ? new RequestConstructor(new HttpRequestBuilders(httpMethod.getHttpRequest(), url, description))
                        .build()
                : new MultipartConstructor(
                        new HttpMultipartRequestBuilders(httpMethod.getHttpRequest(), url, description)).build();
    }

    private String getHost(Host host) throws Exception {
        notNull(host, "Host must not be null");
        String url;
        if (!Strings.isNullOrEmpty(host.value())) {
            url = host.value() + ":" + host.port();
        } else if (!Strings.isNullOrEmpty(host.location())) {
            Properties properties = new Properties();
            properties.load(this.getClass().getClassLoader().getResourceAsStream(host.location()));
            url = properties.getProperty("http.host") + ":" + properties.getProperty("http.port");
        } else {
            throw new IllegalArgumentException("Host is not defined");
        }

        if (!url.startsWith("http://")) {
            url = "http://" + url;
        }
        return url;
    }

    private String getPath(Path path) {
        if (path == null) {
            return "";
        }
        String p = path.value();
        return removeTrailingSlash(p.length() > 0 ? (p.startsWith("/") ? p : "/" + p) : "");
    }

    private String removeTrailingSlash(String path) {
        return path.endsWith("/") ? removeTrailingSlash(path.substring(0, path.length() - 1)) : path;
    }
}