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

reactivefeign.methodhandler.PublisherClientMethodHandler Maven / Gradle / Ivy

/**
 * Copyright 2018 The Feign 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 reactivefeign.methodhandler;

import feign.CollectionFormat;
import feign.MethodMetadata;
import feign.Param;
import feign.QueryMapEncoder;
import feign.RequestTemplate;
import feign.Target;
import feign.querymap.FieldQueryMapEncoder;
import feign.template.UriUtils;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import reactivefeign.client.ReactiveHttpClient;
import reactivefeign.client.ReactiveHttpRequest;
import reactivefeign.publisher.PublisherHttpClient;
import reactivefeign.utils.ContentType;
import reactivefeign.utils.LinkedCaseInsensitiveMap;
import reactivefeign.utils.Pair;
import reactivefeign.utils.SerializedFormData;
import reactor.core.publisher.Mono;

import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static feign.Util.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static reactivefeign.utils.FormUtils.serializeForm;
import static reactivefeign.utils.HttpUtils.CONTENT_TYPE_HEADER;
import static reactivefeign.utils.HttpUtils.FORM_URL_ENCODED;
import static reactivefeign.utils.HttpUtils.MULTIPART_MIME_TYPES;
import static reactivefeign.utils.MultiValueMapUtils.add;
import static reactivefeign.utils.MultiValueMapUtils.addAll;
import static reactivefeign.utils.MultiValueMapUtils.addAllOrdered;
import static reactivefeign.utils.MultiValueMapUtils.addOrdered;
import static reactivefeign.utils.StringUtils.cutPrefix;
import static reactivefeign.utils.StringUtils.cutTail;

/**
 * Method handler for asynchronous HTTP requests via {@link PublisherHttpClient}.
 *
 * Transforms method invocation into request that executed by {@link ReactiveHttpClient}.
 *
 * @author Sergii Karpenko
 */
public class PublisherClientMethodHandler implements MethodHandler {

    public static final Pattern SUBSTITUTION_PATTERN = Pattern.compile("\\{([^}]+)\\}");

    private final Target target;
    private final MethodMetadata methodMetadata;
    private final PublisherHttpClient publisherClient;
    private final Function pathExpander;
    private final Map>>> headerExpanders;
    private final Map> queriesAll;
    private final Map>>> queryExpanders;
    private final URI staticUri;

    private final QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();

    private final Optional contentType;
    private final boolean isMultipart;
    private final boolean isFormUrlEncoded;

    public PublisherClientMethodHandler(Target target,
                                        MethodMetadata methodMetadata,
                                        PublisherHttpClient publisherClient) {
        this.target = checkNotNull(target, "target must be not null");
        this.methodMetadata = checkNotNull(methodMetadata,
                "methodMetadata must be not null");
        this.publisherClient = checkNotNull(publisherClient, "client must be not null");
        RequestTemplate requestTemplate = methodMetadata.template();
        this.pathExpander = buildUrlExpandFunction(requestTemplate, target);
        this.headerExpanders = buildExpanders(requestTemplate.headers());

        this.queriesAll = new HashMap<>(requestTemplate.queries());
        this.contentType = getContentType(requestTemplate.headers());
        this.isMultipart = contentType.map(ct -> MULTIPART_MIME_TYPES.contains(ct.getMediaType())).orElse(false);
        if(isMultipart && methodMetadata.template().bodyTemplate() != null){
            throw new IllegalArgumentException("isMultipart && methodMetadata.template().bodyTemplate() != null");
        }
        this.isFormUrlEncoded = contentType.map(ct -> ct.getMediaType().equalsIgnoreCase(FORM_URL_ENCODED)).orElse(false);
        if(!isMultipart && !isFormUrlEncoded) {
            methodMetadata.formParams()
                    .forEach(param -> add(queriesAll, param, "{" + param + "}"));
        }
        this.queryExpanders = buildExpanders(queriesAll);

        //static template (POST & PUT)
        if(pathExpander instanceof StaticExpander
                && queriesAll.isEmpty()
                && methodMetadata.queryMapIndex() == null){
            staticUri = URI.create(target.url() + cutTail(requestTemplate.url(), "/"));
        } else {
            staticUri = null;
        }

        if(methodMetadata.indexToExpander() == null && methodMetadata.indexToExpanderClass() != null) {
            methodMetadata.indexToExpander(instantiateExpanders(methodMetadata.indexToExpanderClass()));
        }
    }

    @Override
    public Publisher invoke(final Object[] argv) {
        return publisherClient.executeRequest(buildRequest(argv));
    }

    protected ReactiveHttpRequest buildRequest(Object[] argv) {

        Object[] argsExpanded = expandArguments(argv);

        Substitutions substitutions = buildSubstitutions(argsExpanded);

        URI uri = buildUri(argsExpanded, substitutions);

        Map> headers = headers(argsExpanded, substitutions);

        Publisher body = body(argsExpanded, substitutions);

        return new ReactiveHttpRequest(methodMetadata, target, uri, headers, body);
    }

    private URI buildUri(Object[] argv, Substitutions substitutions) {
        //static template
        if(staticUri != null){
            return staticUri;
        }

        String path = pathExpander.apply(substitutions);

        Map> queries = queries(argv, substitutions);
        String queryLine = queryLine(queries);

        return URI.create(path + queryLine);
    }

    private Substitutions buildSubstitutions(Object[] argv) {
        Map substitutions = methodMetadata.indexToName().entrySet().stream()
                .filter(e -> argv[e.getKey()] != null)
                .flatMap(e -> e.getValue().stream()
                        .map(v -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), v)))
                .collect(toMap(Map.Entry::getValue,
                        entry -> argv[entry.getKey()]));

        URI url = methodMetadata.urlIndex() != null ? (URI) argv[methodMetadata.urlIndex()] : null;
        return new Substitutions(substitutions, url);
    }

    private String queryLine(Map> queries) {
        if (queries.isEmpty()) {
            return "";
        }

        StringBuilder queryBuilder = new StringBuilder();
        CollectionFormat collectionFormat = methodMetadata.template().collectionFormat();
        for (Map.Entry> query : queries.entrySet()) {
            Collection valuesEncoded = query.getValue().stream()
                    .map(value -> UriUtils.encode(value, UTF_8))
                    .collect(toList());
            queryBuilder.append('&');
            queryBuilder.append(collectionFormat.join(query.getKey(), valuesEncoded, UTF_8));
        }
        if(queryBuilder.length() > 0) {
            queryBuilder.deleteCharAt(0);
            return queryBuilder.insert(0, '?').toString();
        } else {
            return "";
        }
    }

    protected Map> queries(Object[] argv,
                                                      Substitutions substitutions) {
        Map> queries = new LinkedHashMap<>();

        // queries from template
        queriesAll.keySet().forEach(queryName -> addAll(queries, queryName,
                queryExpanders.getOrDefault(queryName, singletonList(subs ->  singletonList(""))).stream()
                        .map(expander -> expander.apply(substitutions))
                        .filter(Objects::nonNull)
                        .flatMap(Collection::stream)
                        .collect(toList())));

        // queries from args
        if (methodMetadata.queryMapIndex() != null) {
            Object queryMapObject = argv[methodMetadata.queryMapIndex()];
            if(queryMapObject != null){
                Map queryMap = queryMapObject instanceof Map
                        ? (Map) queryMapObject
                        : queryMapEncoder.encode(queryMapObject);
                queryMap.forEach((key, value) -> {
                    if (value instanceof Iterable) {
                        ((Iterable) value).forEach(element -> add(queries, key, element.toString()));
                    } else if (value != null) {
                        add(queries, key, value.toString());
                    }
                });
            }
        }

        return queries;
    }

    protected Map> headers(Object[] argv, Substitutions substitutions) {

        Map> headers = new LinkedCaseInsensitiveMap<>();

        // headers from template
        methodMetadata.template().headers().keySet()
                .forEach(headerName -> addAllOrdered(headers, headerName,
                        headerExpanders.get(headerName).stream()
                                .map(expander -> expander.apply(substitutions))
                                .filter(Objects::nonNull)
                                .flatMap(Collection::stream)
                                .collect(toList())));

        // headers from args
        if (methodMetadata.headerMapIndex() != null) {
            ((Map) argv[methodMetadata.headerMapIndex()])
                    .forEach((key, value) -> {
                        if (value instanceof Iterable) {
                            ((Iterable) value)
                                    .forEach(element -> addOrdered(headers, key, element.toString()));
                        } else {
                            addOrdered(headers, key, value.toString());
                        }
                    });
        }

        return headers;
    }

    protected Publisher body(
            Object[] argv,
            Substitutions substitutions) {

        if(isFormUrlEncoded){
            return serializeFormData(argv, substitutions);
        }

        if (methodMetadata.bodyIndex() != null) {
            Object body = argv[methodMetadata.bodyIndex()];
            return body(body);
        } else if(isMultipart) { //all arguments have Param annotation
            return new MultipartMap(collectFormData(substitutions));
        } else {
            return Mono.empty();
        }
    }

    private SerializedFormData serializeFormData(Object[] argv, Substitutions substitutions) {
        Map formData;
        if (methodMetadata.bodyIndex() != null) {
            Object body = argv[methodMetadata.bodyIndex()];
            formData = (Map)body;
        } else {
            formData = collectFormData(substitutions);
        }
        return serializeForm(formData, contentType.get().getCharset());
    }

    private Map> collectFormData(Substitutions substitutions) {
        Map> formVariables = new LinkedHashMap<>();
        for (Map.Entry entry : substitutions.placeholderToSubstitution.entrySet()) {
            if (methodMetadata.formParams().contains(entry.getKey())) {
                formVariables.put(entry.getKey(), singletonList(entry.getValue()));
            }
        }
        return formVariables;
    }

    protected Publisher body(Object body) {
        if (body instanceof Publisher) {
            return (Publisher) body;
        } else {
            return Mono.just(body);
        }
    }

    private static Map>>> buildExpanders(
            Map> templates) {
        Stream> templatesFlattened = templates.entrySet().stream()
                .flatMap(e -> e.getValue().stream()
                        .map(v -> new Pair<>(e.getKey(), v)));
        return templatesFlattened.collect(groupingBy(
                entry -> entry.left,
                mapping(entry -> buildMultiValueExpandFunction(entry.right), toList())));
    }

    /**
     *
     * @param template
     * @return function that able to map substitutions map to actual value for specified template
     */
    private static Function> buildMultiValueExpandFunction(String template) {
        Matcher matcher = SUBSTITUTION_PATTERN.matcher(template);
        if(matcher.matches()){
            String placeholder = matcher.group(1);

            return substitutions -> {
                Object substitution = substitutions.placeholderToSubstitution.get(placeholder);
                if (substitution != null) {
                    if(substitution instanceof Iterable){
                        List stringValues = new ArrayList<>();
                        ((Iterable) substitution).forEach(o -> stringValues.add(o.toString()));
                        return stringValues;
                    } else if(substitution instanceof Object[]){
                        List stringValues = new ArrayList<>(((Object[]) substitution).length);
                        (asList((Object[])substitution)).forEach(o -> stringValues.add(o.toString()));
                        return stringValues;
                    }
                    else {
                        return singletonList(substitution.toString());
                    }
                } else {
                    return null;
                }
            };
        } else {
            Function expandFunction = buildExpandFunction(template);
            return substitutions -> singletonList(expandFunction.apply(substitutions));
        }
    }

    private static Function buildUrlExpandFunction(
            RequestTemplate requestTemplate, Target target) {
        String requestUrl = getRequestUrl(requestTemplate);

        if(target instanceof Target.EmptyTarget){
            return expandUrlForEmptyTarget(buildExpandFunction(requestUrl));
        } else {
            String targetUrl = cutTail(target.url(), "/");
            return buildExpandFunction(targetUrl+requestUrl);
        }
    }

    private static String getRequestUrl(RequestTemplate requestTemplate) {
        String requestUrl = cutTail(requestTemplate.url(), requestTemplate.queryLine());
        requestUrl = cutPrefix(requestUrl, "/");
        if(!requestUrl.isEmpty()){
            requestUrl = "/" + requestUrl;
        }
        return requestUrl;
    }

    private static Function expandUrlForEmptyTarget(
            Function expandFunction){
        return substitutions -> substitutions.url.toString() + expandFunction.apply(substitutions);
    }

    /**
     *
     * @param template
     * @return function that able to map substitutions map to actual value for specified template
     */
    private static Function buildExpandFunction(String template) {
        List> chunks = new ArrayList<>();
        Matcher matcher = SUBSTITUTION_PATTERN.matcher(template);
        int previousMatchEnd = 0;
        while (matcher.find()) {
            String textChunk = template.substring(previousMatchEnd, matcher.start());
            if (textChunk.length() > 0) {
                chunks.add(data -> textChunk);
            }

            String placeholder = matcher.group(1);
            chunks.add(data -> {
                Object substitution = data.placeholderToSubstitution.get(placeholder);
                if (substitution != null) {
                    return UriUtils.encode(substitution.toString(), UTF_8);
                } else {
                    throw new IllegalArgumentException("No substitution in url for:"+placeholder);
                }
            });
            previousMatchEnd = matcher.end();
        }

        //no substitutions in template
        if(previousMatchEnd == 0){
            return new StaticExpander(template);
        }

        String textChunk = template.substring(previousMatchEnd);
        if (textChunk.length() > 0) {
            chunks.add(data -> textChunk);
        }

        return substitutions -> chunks.stream().map(chunk -> chunk.apply(substitutions))
                .collect(Collectors.joining());
    }

    private static class StaticExpander implements Function{

        private final String staticPath;

        private StaticExpander(String staticPath) {
            this.staticPath = staticPath;
        }

        @Override
        public String apply(Substitutions substitutions) {
            return staticPath;
        }
    }

    private static class Substitutions {
        private final URI url;
        private final Map placeholderToSubstitution;

        private Substitutions(Map placeholderToSubstitution, URI url) {
            this.url = url;
            this.placeholderToSubstitution = placeholderToSubstitution;
        }
    }

    private Map instantiateExpanders(Map> indexToExpanderClass) {
        Map indexToExpanderMap = new HashMap<>(indexToExpanderClass.size());
        indexToExpanderClass.forEach((index, expanderClass) -> {
            try {
                indexToExpanderMap.put(index, expanderClass.getDeclaredConstructor().newInstance());
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                throw new IllegalStateException(e);
            }
        });
        return indexToExpanderMap;
    }

    private Object[] expandArguments(Object[] args) {
        if(args == null || args.length == 0){
            return args;
        }

        Map integerExpanderMap = methodMetadata.indexToExpander();
        if(integerExpanderMap == null || integerExpanderMap.size() == 0){
            return args;
        }
        Object[] argsExpanded = new Object[args.length];
        for(int i = 0, n = args.length; i < n; i++){
            Param.Expander expander = integerExpanderMap.get(i);
            if(expander != null){
                argsExpanded[i] = expandElements(expander, args[i]);
            } else {
                argsExpanded[i] = args[i];
            }
        }
        return argsExpanded;
    }

    private Object expandElements(Param.Expander expander, Object value) {
        if (value instanceof Iterable) {
            return expandIterable(expander, (Iterable) value);
        }
        return expander.expand(value);
    }

    private List expandIterable(Param.Expander expander, Iterable value) {
        List values = new ArrayList();
        for (Object element : value) {
            if (element != null) {
                values.add(expander.expand(element));
            }
        }
        return values;
    }

    public static class MultipartMap implements Publisher {

        private final Map> map;

        public MultipartMap(Map> map) {
            this.map = map;
        }

        public Map> getMap() {
            return map;
        }

        @Override
        public void subscribe(Subscriber s) {
            throw new UnsupportedOperationException();
        }
    }

    public Optional getContentType(Map> headers){
        return headers.entrySet().stream()
                .filter(entry -> entry.getKey().equalsIgnoreCase(CONTENT_TYPE_HEADER))
                .map(entry -> ContentType.parse(entry.getValue().iterator().next().toLowerCase()))
                .findFirst();
    }
}