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.annotation.support;
018
019import java.lang.annotation.Annotation;
020
021import org.springframework.core.MethodParameter;
022import org.springframework.core.annotation.AnnotationUtils;
023import org.springframework.lang.Nullable;
024import org.springframework.messaging.Message;
025import org.springframework.messaging.converter.MessageConversionException;
026import org.springframework.messaging.converter.MessageConverter;
027import org.springframework.messaging.converter.SmartMessageConverter;
028import org.springframework.messaging.handler.annotation.Payload;
029import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
030import org.springframework.util.Assert;
031import org.springframework.util.ClassUtils;
032import org.springframework.util.ObjectUtils;
033import org.springframework.util.StringUtils;
034import org.springframework.validation.BeanPropertyBindingResult;
035import org.springframework.validation.BindingResult;
036import org.springframework.validation.ObjectError;
037import org.springframework.validation.SmartValidator;
038import org.springframework.validation.Validator;
039import org.springframework.validation.annotation.Validated;
040
041/**
042 * A resolver to extract and convert the payload of a message using a
043 * {@link MessageConverter}. It also validates the payload using a
044 * {@link Validator} if the argument is annotated with a Validation annotation.
045 *
046 * <p>This {@link HandlerMethodArgumentResolver} should be ordered last as it
047 * supports all types and does not require the {@link Payload} annotation.
048 *
049 * @author Rossen Stoyanchev
050 * @author Juergen Hoeller
051 * @author Brian Clozel
052 * @author Stephane Nicoll
053 * @since 5.2
054 */
055public class PayloadMethodArgumentResolver implements HandlerMethodArgumentResolver {
056
057        private final MessageConverter converter;
058
059        @Nullable
060        private final Validator validator;
061
062        private final boolean useDefaultResolution;
063
064
065        /**
066         * Create a new {@code PayloadArgumentResolver} with the given
067         * {@link MessageConverter}.
068         * @param messageConverter the MessageConverter to use (required)
069         */
070        public PayloadMethodArgumentResolver(MessageConverter messageConverter) {
071                this(messageConverter, null);
072        }
073
074        /**
075         * Create a new {@code PayloadArgumentResolver} with the given
076         * {@link MessageConverter} and {@link Validator}.
077         * @param messageConverter the MessageConverter to use (required)
078         * @param validator the Validator to use (optional)
079         */
080        public PayloadMethodArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator) {
081                this(messageConverter, validator, true);
082        }
083
084        /**
085         * Create a new {@code PayloadArgumentResolver} with the given
086         * {@link MessageConverter} and {@link Validator}.
087         * @param messageConverter the MessageConverter to use (required)
088         * @param validator the Validator to use (optional)
089         * @param useDefaultResolution if "true" (the default) this resolver supports
090         * all parameters; if "false" then only arguments with the {@code @Payload}
091         * annotation are supported.
092         */
093        public PayloadMethodArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator,
094                        boolean useDefaultResolution) {
095
096                Assert.notNull(messageConverter, "MessageConverter must not be null");
097                this.converter = messageConverter;
098                this.validator = validator;
099                this.useDefaultResolution = useDefaultResolution;
100        }
101
102
103        @Override
104        public boolean supportsParameter(MethodParameter parameter) {
105                return (parameter.hasParameterAnnotation(Payload.class) || this.useDefaultResolution);
106        }
107
108        @Override
109        @Nullable
110        public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
111                Payload ann = parameter.getParameterAnnotation(Payload.class);
112                if (ann != null && StringUtils.hasText(ann.expression())) {
113                        throw new IllegalStateException("@Payload SpEL expressions not supported by this resolver");
114                }
115
116                Object payload = message.getPayload();
117                if (isEmptyPayload(payload)) {
118                        if (ann == null || ann.required()) {
119                                String paramName = getParameterName(parameter);
120                                BindingResult bindingResult = new BeanPropertyBindingResult(payload, paramName);
121                                bindingResult.addError(new ObjectError(paramName, "Payload value must not be empty"));
122                                throw new MethodArgumentNotValidException(message, parameter, bindingResult);
123                        }
124                        else {
125                                return null;
126                        }
127                }
128
129                Class<?> targetClass = resolveTargetClass(parameter, message);
130                Class<?> payloadClass = payload.getClass();
131                if (ClassUtils.isAssignable(targetClass, payloadClass)) {
132                        validate(message, parameter, payload);
133                        return payload;
134                }
135                else {
136                        if (this.converter instanceof SmartMessageConverter) {
137                                SmartMessageConverter smartConverter = (SmartMessageConverter) this.converter;
138                                payload = smartConverter.fromMessage(message, targetClass, parameter);
139                        }
140                        else {
141                                payload = this.converter.fromMessage(message, targetClass);
142                        }
143                        if (payload == null) {
144                                throw new MessageConversionException(message, "Cannot convert from [" +
145                                                payloadClass.getName() + "] to [" + targetClass.getName() + "] for " + message);
146                        }
147                        validate(message, parameter, payload);
148                        return payload;
149                }
150        }
151
152        private String getParameterName(MethodParameter param) {
153                String paramName = param.getParameterName();
154                return (paramName != null ? paramName : "Arg " + param.getParameterIndex());
155        }
156
157        /**
158         * Specify if the given {@code payload} is empty.
159         * @param payload the payload to check (can be {@code null})
160         */
161        protected boolean isEmptyPayload(@Nullable Object payload) {
162                if (payload == null) {
163                        return true;
164                }
165                else if (payload instanceof byte[]) {
166                        return ((byte[]) payload).length == 0;
167                }
168                else if (payload instanceof String) {
169                        return !StringUtils.hasText((String) payload);
170                }
171                else {
172                        return false;
173                }
174        }
175
176        /**
177         * Resolve the target class to convert the payload to.
178         * <p>By default this is simply {@link MethodParameter#getParameterType()}
179         * but that can be overridden to select a more specific target type after
180         * also taking into account the "Content-Type", e.g. return {@code String}
181         * if target type is {@code Object} and {@code "Content-Type:text/**"}.
182         * @param parameter the target method parameter
183         * @param message the message being processed
184         * @return the target type to use
185         * @since 5.2
186         */
187        protected Class<?> resolveTargetClass(MethodParameter parameter, Message<?> message) {
188                return parameter.getParameterType();
189        }
190
191        /**
192         * Validate the payload if applicable.
193         * <p>The default implementation checks for {@code @javax.validation.Valid},
194         * Spring's {@link Validated},
195         * and custom annotations whose name starts with "Valid".
196         * @param message the currently processed message
197         * @param parameter the method parameter
198         * @param target the target payload object
199         * @throws MethodArgumentNotValidException in case of binding errors
200         */
201        protected void validate(Message<?> message, MethodParameter parameter, Object target) {
202                if (this.validator == null) {
203                        return;
204                }
205                for (Annotation ann : parameter.getParameterAnnotations()) {
206                        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
207                        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
208                                Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
209                                Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
210                                BeanPropertyBindingResult bindingResult =
211                                                new BeanPropertyBindingResult(target, getParameterName(parameter));
212                                if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) {
213                                        ((SmartValidator) this.validator).validate(target, bindingResult, validationHints);
214                                }
215                                else {
216                                        this.validator.validate(target, bindingResult);
217                                }
218                                if (bindingResult.hasErrors()) {
219                                        throw new MethodArgumentNotValidException(message, parameter, bindingResult);
220                                }
221                                break;
222                        }
223                }
224        }
225
226}