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

io.github.danielliu1123.httpexchange.shaded.ShadedHttpServiceMethod Maven / Gradle / Ivy

There is a newer version: 3.4.0-RC1
Show newest version
/*
 * Copyright 2002-2023 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
 *
 *      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.github.danielliu1123.httpexchange.shaded;

import jakarta.annotation.Nullable;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.service.invoker.HttpExchangeAdapter;
import org.springframework.web.service.invoker.HttpRequestValues;
import org.springframework.web.service.invoker.HttpServiceArgumentResolver;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.service.invoker.ReactiveHttpRequestValues;
import org.springframework.web.service.invoker.ReactorHttpExchangeAdapter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * Implements the invocation of an {@link org.springframework.web.bind.annotation.RequestMapping @RequestMapping}-annotated,
 * {@link HttpServiceProxyFactory#createClient(Class) HTTP service proxy} method
 * by delegating to an {@link HttpExchangeAdapter} to perform actual requests.
 *
 * @author Rossen Stoyanchev
 * @author Sebastien Deleuze
 * @author Olga Maciaszek-Sharma
 * @since 6.0
 */
final class ShadedHttpServiceMethod {

    private static final boolean REACTOR_PRESENT =
            ClassUtils.isPresent("reactor.core.publisher.Mono", ShadedHttpServiceMethod.class.getClassLoader());

    private final Method method;

    private final MethodParameter[] parameters;

    private final List argumentResolvers;

    private final HttpRequestValuesInitializer requestValuesInitializer;

    private final ResponseFunction responseFunction;

    ShadedHttpServiceMethod(
            Method method,
            Class containingClass,
            List argumentResolvers,
            HttpExchangeAdapter adapter,
            @Nullable StringValueResolver embeddedValueResolver) {

        this.method = method;
        this.parameters = initMethodParameters(method);
        this.argumentResolvers = argumentResolvers;

        boolean isReactorAdapter = (REACTOR_PRESENT && adapter instanceof ReactorHttpExchangeAdapter);

        this.requestValuesInitializer = HttpRequestValuesInitializer.create(
                method,
                containingClass,
                embeddedValueResolver,
                (isReactorAdapter ? ReactiveHttpRequestValues::builder : HttpRequestValues::builder));

        this.responseFunction = (isReactorAdapter
                ? ReactorExchangeResponseFunction.create((ReactorHttpExchangeAdapter) adapter, method)
                : ExchangeResponseFunction.create(adapter, method));
    }

    private static MethodParameter[] initMethodParameters(Method method) {
        int count = method.getParameterCount();
        if (count == 0) {
            return new MethodParameter[0];
        }
        if (KotlinDetector.isSuspendingFunction(method)) {
            count -= 1;
        }

        DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
        MethodParameter[] parameters = new MethodParameter[count];
        for (int i = 0; i < count; i++) {
            parameters[i] = new SynthesizingMethodParameter(method, i);
            parameters[i].initParameterNameDiscovery(nameDiscoverer);
        }
        return parameters;
    }

    public Method getMethod() {
        return this.method;
    }

    @Nullable
    public Object invoke(Object[] arguments) {
        HttpRequestValues.Builder requestValues = this.requestValuesInitializer.initializeRequestValuesBuilder();
        applyArguments(requestValues, arguments);
        return this.responseFunction.execute(requestValues.build());
    }

    private void applyArguments(HttpRequestValues.Builder requestValues, Object[] arguments) {
        Assert.isTrue(arguments.length == this.parameters.length, "Method argument mismatch");
        for (int i = 0; i < arguments.length; i++) {
            Object value = arguments[i];
            boolean resolved = false;
            for (HttpServiceArgumentResolver resolver : this.argumentResolvers) {
                if (resolver.resolve(value, this.parameters[i], requestValues)) {
                    resolved = true;
                    break;
                }
            }
            int index = i;
            Assert.state(
                    resolved,
                    () -> "Could not resolve parameter [" + this.parameters[index].getParameterIndex() + "] in "
                            + this.parameters[index].getExecutable().toGenericString()
                            + (StringUtils.hasText("No suitable resolver") ? ": " + "No suitable resolver" : ""));
        }
    }

    /**
     * Factory for {@link HttpRequestValues} with values extracted from the type
     * and method-level {@link org.springframework.web.bind.annotation.RequestMapping @HttpRequest} annotations.
     */
    private record HttpRequestValuesInitializer(
            @Nullable HttpMethod httpMethod,
            @Nullable String url,
            @Nullable MediaType contentType,
            @Nullable List acceptMediaTypes,
            Supplier requestValuesSupplier) {

        public HttpRequestValues.Builder initializeRequestValuesBuilder() {
            HttpRequestValues.Builder requestValues = this.requestValuesSupplier.get();
            if (this.httpMethod != null) {
                requestValues.setHttpMethod(this.httpMethod);
            }
            if (this.url != null) {
                requestValues.setUriTemplate(this.url);
            }
            if (this.contentType != null) {
                requestValues.setContentType(this.contentType);
            }
            if (this.acceptMediaTypes != null) {
                requestValues.setAccept(this.acceptMediaTypes);
            }
            return requestValues;
        }

        /**
         * Introspect the method and create the request factory for it.
         */
        public static HttpRequestValuesInitializer create(
                Method method,
                Class containingClass,
                @Nullable StringValueResolver embeddedValueResolver,
                Supplier requestValuesSupplier) {

            RequestMapping annot1 = AnnotatedElementUtils.findMergedAnnotation(containingClass, RequestMapping.class);
            RequestMapping annot2 = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);

            Assert.notNull(annot2, "Expected HttpRequest annotation");

            HttpMethod httpMethod = initHttpMethod(annot1, annot2);
            String url = initUrl(annot1, annot2, embeddedValueResolver);
            MediaType contentType = initContentType(annot1, annot2);
            List acceptableMediaTypes = initAccept(annot1, annot2);

            return new HttpRequestValuesInitializer(
                    httpMethod, url, contentType, acceptableMediaTypes, requestValuesSupplier);
        }

        @Nullable
        private static HttpMethod initHttpMethod(@Nullable RequestMapping typeAnnot, RequestMapping annot) {

            String value1 = (typeAnnot != null ? typeAnnot.method().length : 0) > 0
                    ? typeAnnot.method()[0].asHttpMethod().name()
                    : null;
            String value2 = (annot.method().length > 0)
                    ? annot.method()[0].asHttpMethod().name()
                    : null;

            if (StringUtils.hasText(value2)) {
                return HttpMethod.valueOf(value2);
            }

            if (StringUtils.hasText(value1)) {
                return HttpMethod.valueOf(value1);
            }

            return null;
        }

        @Nullable
        private static String initUrl(
                @Nullable RequestMapping typeAnnot,
                RequestMapping annot,
                @Nullable StringValueResolver embeddedValueResolver) {

            String url1 = (typeAnnot != null ? typeAnnot.value().length : 0) > 0
                    ? typeAnnot.value()[0]
                    : null;
            String url2 = annot.value()[0];

            if (embeddedValueResolver != null) {
                url1 = (url1 != null ? embeddedValueResolver.resolveStringValue(url1) : null);
                url2 = embeddedValueResolver.resolveStringValue(url2);
            }

            boolean hasUrl1 = StringUtils.hasText(url1);
            boolean hasUrl2 = StringUtils.hasText(url2);

            if (hasUrl1 && hasUrl2) {
                return (url1 + (!url1.endsWith("/") && !url2.startsWith("/") ? "/" : "") + url2);
            }

            if (!hasUrl1 && !hasUrl2) {
                return null;
            }

            return (hasUrl2 ? url2 : url1);
        }

        @Nullable
        private static MediaType initContentType(@Nullable RequestMapping typeAnnot, RequestMapping annot) {

            String value1 = (typeAnnot != null ? typeAnnot.consumes().length : 0) > 0
                    ? typeAnnot.consumes()[0]
                    : "";
            String value2 = (annot.consumes().length > 0) ? annot.consumes()[0] : "";

            if (StringUtils.hasText(value2)) {
                return MediaType.parseMediaType(value2);
            }

            if (StringUtils.hasText(value1)) {
                return MediaType.parseMediaType(value1);
            }

            return null;
        }

        @Nullable
        private static List initAccept(@Nullable RequestMapping typeAnnot, RequestMapping annot) {

            String[] value1 = (typeAnnot != null ? typeAnnot.produces() : null);
            String[] value2 = annot.produces();

            if (!ObjectUtils.isEmpty(value2)) {
                return MediaType.parseMediaTypes(Arrays.asList(value2));
            }

            if (!ObjectUtils.isEmpty(value1)) {
                return MediaType.parseMediaTypes(Arrays.asList(value1));
            }

            return null;
        }
    }

    /**
     * Execute a request, obtain a response, and adapt to the expected return type.
     */
    private interface ResponseFunction {

        @Nullable
        Object execute(HttpRequestValues requestValues);
    }

    private record ExchangeResponseFunction(Function responseFunction)
            implements ResponseFunction {

        @Override
        public Object execute(HttpRequestValues requestValues) {
            return this.responseFunction.apply(requestValues);
        }

        /**
         * Create the {@code ResponseFunction} that matches the method return type.
         */
        public static ResponseFunction create(HttpExchangeAdapter client, Method method) {
            if (KotlinDetector.isSuspendingFunction(method)) {
                throw new IllegalStateException("Kotlin Coroutines are only supported with reactive implementations");
            }

            MethodParameter param = new MethodParameter(method, -1).nestedIfOptional();
            Class paramType = param.getNestedParameterType();

            Function responseFunction;
            if (paramType.equals(void.class) || paramType.equals(Void.class)) {
                responseFunction = requestValues -> {
                    client.exchange(requestValues);
                    return null;
                };
            } else if (paramType.equals(HttpHeaders.class)) {
                responseFunction = request -> asOptionalIfNecessary(client.exchangeForHeaders(request), param);
            } else if (paramType.equals(ResponseEntity.class)) {
                MethodParameter bodyParam = param.nested();
                if (bodyParam.getNestedParameterType().equals(Void.class)) {
                    responseFunction =
                            request -> asOptionalIfNecessary(client.exchangeForBodilessEntity(request), param);
                } else {
                    ParameterizedTypeReference bodyTypeRef =
                            ParameterizedTypeReference.forType(bodyParam.getNestedGenericParameterType());
                    responseFunction =
                            request -> asOptionalIfNecessary(client.exchangeForEntity(request, bodyTypeRef), param);
                }
            } else {
                ParameterizedTypeReference bodyTypeRef =
                        ParameterizedTypeReference.forType(param.getNestedGenericParameterType());
                responseFunction =
                        request -> asOptionalIfNecessary(client.exchangeForBody(request, bodyTypeRef), param);
            }

            return new ExchangeResponseFunction(responseFunction);
        }

        private static @Nullable Object asOptionalIfNecessary(@Nullable Object response, MethodParameter param) {
            return param.getParameterType().equals(Optional.class) ? Optional.ofNullable(response) : response;
        }
    }

    /**
     * {@link ResponseFunction} for {@link ReactorHttpExchangeAdapter}.
     */
    private record ReactorExchangeResponseFunction(
            Function> responseFunction,
            @Nullable ReactiveAdapter returnTypeAdapter,
            boolean blockForOptional,
            @Nullable Duration blockTimeout)
            implements ResponseFunction {

        @Nullable
        public Object execute(HttpRequestValues requestValues) {

            Publisher responsePublisher = this.responseFunction.apply(requestValues);

            if (this.returnTypeAdapter != null) {
                return this.returnTypeAdapter.fromPublisher(responsePublisher);
            }

            if (this.blockForOptional) {
                return (this.blockTimeout != null
                        ? ((Mono) responsePublisher).blockOptional(this.blockTimeout)
                        : ((Mono) responsePublisher).blockOptional());
            } else {
                return (this.blockTimeout != null
                        ? ((Mono) responsePublisher).block(this.blockTimeout)
                        : ((Mono) responsePublisher).block());
            }
        }

        /**
         * Create the {@code ResponseFunction} that matches the method return type.
         */
        public static ResponseFunction create(ReactorHttpExchangeAdapter client, Method method) {
            MethodParameter returnParam = new MethodParameter(method, -1);
            Class returnType = returnParam.getParameterType();
            boolean isSuspending = KotlinDetector.isSuspendingFunction(method);
            if (isSuspending) {
                returnType = Mono.class;
            }

            ReactiveAdapter reactiveAdapter =
                    client.getReactiveAdapterRegistry().getAdapter(returnType);

            MethodParameter actualParam =
                    (reactiveAdapter != null ? returnParam.nested() : returnParam.nestedIfOptional());
            Class actualType = isSuspending ? actualParam.getParameterType() : actualParam.getNestedParameterType();

            Function> responseFunction;
            if (actualType.equals(void.class) || actualType.equals(Void.class)) {
                responseFunction = client::exchangeForMono;
            } else if (reactiveAdapter != null && reactiveAdapter.isNoValue()) {
                responseFunction = client::exchangeForMono;
            } else if (actualType.equals(HttpHeaders.class)) {
                responseFunction = client::exchangeForHeadersMono;
            } else if (actualType.equals(ResponseEntity.class)) {
                MethodParameter bodyParam = isSuspending ? actualParam : actualParam.nested();
                Class bodyType = bodyParam.getNestedParameterType();
                if (bodyType.equals(Void.class)) {
                    responseFunction = client::exchangeForBodilessEntityMono;
                } else {
                    ReactiveAdapter bodyAdapter =
                            client.getReactiveAdapterRegistry().getAdapter(bodyType);
                    responseFunction = initResponseEntityFunction(client, bodyParam, bodyAdapter, isSuspending);
                }
            } else {
                responseFunction = initBodyFunction(client, actualParam, reactiveAdapter, isSuspending);
            }

            return new ReactorExchangeResponseFunction(
                    responseFunction, reactiveAdapter, returnType.equals(Optional.class), client.getBlockTimeout());
        }

        @SuppressWarnings("ConstantConditions")
        private static Function> initResponseEntityFunction(
                ReactorHttpExchangeAdapter client,
                MethodParameter methodParam,
                @Nullable ReactiveAdapter reactiveAdapter,
                boolean isSuspending) {

            if (reactiveAdapter == null) {
                return request -> client.exchangeForEntityMono(
                        request, ParameterizedTypeReference.forType(methodParam.getNestedGenericParameterType()));
            }

            Assert.isTrue(
                    reactiveAdapter.isMultiValue(),
                    "ResponseEntity body must be a concrete value or a multi-value Publisher");

            ParameterizedTypeReference bodyType = ParameterizedTypeReference.forType(
                    isSuspending
                            ? methodParam.nested().getGenericParameterType()
                            : methodParam.nested().getNestedGenericParameterType());

            // Shortcut for Flux
            if (reactiveAdapter.getReactiveType().equals(Flux.class)) {
                return request -> client.exchangeForEntityFlux(request, bodyType);
            }

            return request -> client.exchangeForEntityFlux(request, bodyType).map(entity -> {
                Object body = reactiveAdapter.fromPublisher(entity.getBody());
                return new ResponseEntity<>(body, entity.getHeaders(), entity.getStatusCode());
            });
        }

        private static Function> initBodyFunction(
                ReactorHttpExchangeAdapter client,
                MethodParameter methodParam,
                @Nullable ReactiveAdapter reactiveAdapter,
                boolean isSuspending) {

            ParameterizedTypeReference bodyType = ParameterizedTypeReference.forType(
                    isSuspending ? methodParam.getGenericParameterType() : methodParam.getNestedGenericParameterType());

            return (reactiveAdapter != null && reactiveAdapter.isMultiValue()
                    ? request -> client.exchangeForBodyFlux(request, bodyType)
                    : request -> client.exchangeForBodyMono(request, bodyType));
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy