001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.messaging.handler.invocation.reactive;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.lang.reflect.ParameterizedType;
022import java.lang.reflect.Type;
023import java.util.ArrayList;
024import java.util.List;
025import java.util.stream.Stream;
026
027import reactor.core.publisher.Mono;
028
029import org.springframework.core.CoroutinesUtils;
030import org.springframework.core.DefaultParameterNameDiscoverer;
031import org.springframework.core.KotlinDetector;
032import org.springframework.core.MethodParameter;
033import org.springframework.core.ParameterNameDiscoverer;
034import org.springframework.core.ReactiveAdapter;
035import org.springframework.core.ReactiveAdapterRegistry;
036import org.springframework.lang.Nullable;
037import org.springframework.messaging.Message;
038import org.springframework.messaging.handler.HandlerMethod;
039import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException;
040import org.springframework.util.ObjectUtils;
041import org.springframework.util.ReflectionUtils;
042
043/**
044 * Extension of {@link HandlerMethod} that invokes the underlying method with
045 * argument values resolved from the current HTTP request through a list of
046 * {@link HandlerMethodArgumentResolver}.
047 *
048 * @author Rossen Stoyanchev
049 * @since 5.2
050 */
051public class InvocableHandlerMethod extends HandlerMethod {
052
053        private static final Mono<Object[]> EMPTY_ARGS = Mono.just(new Object[0]);
054
055        private static final Object NO_ARG_VALUE = new Object();
056
057
058        private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
059
060        private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
061
062        private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
063
064
065        /**
066         * Create an instance from a {@code HandlerMethod}.
067         */
068        public InvocableHandlerMethod(HandlerMethod handlerMethod) {
069                super(handlerMethod);
070        }
071
072        /**
073         * Create an instance from a bean instance and a method.
074         */
075        public InvocableHandlerMethod(Object bean, Method method) {
076                super(bean, method);
077        }
078
079
080        /**
081         * Configure the argument resolvers to use to use for resolving method
082         * argument values against a {@code ServerWebExchange}.
083         */
084        public void setArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
085                this.resolvers.addResolvers(resolvers);
086        }
087
088        /**
089         * Return the configured argument resolvers.
090         */
091        public List<HandlerMethodArgumentResolver> getResolvers() {
092                return this.resolvers.getResolvers();
093        }
094
095        /**
096         * Set the ParameterNameDiscoverer for resolving parameter names when needed
097         * (e.g. default request attribute name).
098         * <p>Default is a {@link DefaultParameterNameDiscoverer}.
099         */
100        public void setParameterNameDiscoverer(ParameterNameDiscoverer nameDiscoverer) {
101                this.parameterNameDiscoverer = nameDiscoverer;
102        }
103
104        /**
105         * Return the configured parameter name discoverer.
106         */
107        public ParameterNameDiscoverer getParameterNameDiscoverer() {
108                return this.parameterNameDiscoverer;
109        }
110
111        /**
112         * Configure a reactive adapter registry. This is needed for async return values.
113         * <p>By default this is a {@link ReactiveAdapterRegistry} with default settings.
114         */
115        public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
116                this.reactiveAdapterRegistry = registry;
117        }
118
119
120        /**
121         * Invoke the method for the given exchange.
122         * @param message the current message
123         * @param providedArgs optional list of argument values to match by type
124         * @return a Mono with the result from the invocation
125         */
126        @SuppressWarnings("KotlinInternalInJava")
127        public Mono<Object> invoke(Message<?> message, Object... providedArgs) {
128                return getMethodArgumentValues(message, providedArgs).flatMap(args -> {
129                        Object value;
130                        boolean isSuspendingFunction = false;
131                        try {
132                                Method method = getBridgedMethod();
133                                ReflectionUtils.makeAccessible(method);
134                                if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(method.getDeclaringClass())
135                                                && CoroutinesUtils.isSuspendingFunction(method)) {
136                                        isSuspendingFunction = true;
137                                        value = CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
138                                }
139                                else {
140                                        value = method.invoke(getBean(), args);
141                                }
142                        }
143                        catch (IllegalArgumentException ex) {
144                                assertTargetBean(getBridgedMethod(), getBean(), args);
145                                String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
146                                return Mono.error(new IllegalStateException(formatInvokeError(text, args), ex));
147                        }
148                        catch (InvocationTargetException ex) {
149                                return Mono.error(ex.getTargetException());
150                        }
151                        catch (Throwable ex) {
152                                // Unlikely to ever get here, but it must be handled...
153                                return Mono.error(new IllegalStateException(formatInvokeError("Invocation failure", args), ex));
154                        }
155
156                        MethodParameter returnType = getReturnType();
157                        Class<?> reactiveType = (isSuspendingFunction ? value.getClass() : returnType.getParameterType());
158                        ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
159                        return (isAsyncVoidReturnType(returnType, adapter) ?
160                                        Mono.from(adapter.toPublisher(value)) : Mono.justOrEmpty(value));
161                });
162        }
163
164        private Mono<Object[]> getMethodArgumentValues(Message<?> message, Object... providedArgs) {
165                MethodParameter[] parameters = getMethodParameters();
166                if (ObjectUtils.isEmpty(getMethodParameters())) {
167                        return EMPTY_ARGS;
168                }
169
170                List<Mono<Object>> argMonos = new ArrayList<>(parameters.length);
171                for (MethodParameter parameter : parameters) {
172                        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
173                        Object providedArg = findProvidedArgument(parameter, providedArgs);
174                        if (providedArg != null) {
175                                argMonos.add(Mono.just(providedArg));
176                                continue;
177                        }
178                        if (!this.resolvers.supportsParameter(parameter)) {
179                                return Mono.error(new MethodArgumentResolutionException(
180                                                message, parameter, formatArgumentError(parameter, "No suitable resolver")));
181                        }
182                        try {
183                                argMonos.add(this.resolvers.resolveArgument(parameter, message)
184                                                .defaultIfEmpty(NO_ARG_VALUE)
185                                                .doOnError(ex -> logArgumentErrorIfNecessary(parameter, ex)));
186                        }
187                        catch (Exception ex) {
188                                logArgumentErrorIfNecessary(parameter, ex);
189                                argMonos.add(Mono.error(ex));
190                        }
191                }
192                return Mono.zip(argMonos, values ->
193                                Stream.of(values).map(value -> value != NO_ARG_VALUE ? value : null).toArray());
194        }
195
196        private void logArgumentErrorIfNecessary(MethodParameter parameter, Throwable ex) {
197                // Leave stack trace for later, if error is not handled...
198                String exMsg = ex.getMessage();
199                if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
200                        if (logger.isDebugEnabled()) {
201                                logger.debug(formatArgumentError(parameter, exMsg));
202                        }
203                }
204        }
205
206        private boolean isAsyncVoidReturnType(MethodParameter returnType, @Nullable ReactiveAdapter reactiveAdapter) {
207                if (reactiveAdapter != null && reactiveAdapter.supportsEmpty()) {
208                        if (reactiveAdapter.isNoValue()) {
209                                return true;
210                        }
211                        Type parameterType = returnType.getGenericParameterType();
212                        if (parameterType instanceof ParameterizedType) {
213                                ParameterizedType type = (ParameterizedType) parameterType;
214                                if (type.getActualTypeArguments().length == 1) {
215                                        return Void.class.equals(type.getActualTypeArguments()[0]);
216                                }
217                        }
218                }
219                return false;
220        }
221
222}