001/*
002 * Copyright 2002-2020 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.util.Map;
020import java.util.concurrent.ConcurrentHashMap;
021
022import org.springframework.beans.factory.BeanFactory;
023import org.springframework.beans.factory.config.BeanExpressionContext;
024import org.springframework.beans.factory.config.BeanExpressionResolver;
025import org.springframework.beans.factory.config.ConfigurableBeanFactory;
026import org.springframework.core.MethodParameter;
027import org.springframework.core.convert.ConversionService;
028import org.springframework.core.convert.TypeDescriptor;
029import org.springframework.core.convert.support.DefaultConversionService;
030import org.springframework.lang.Nullable;
031import org.springframework.messaging.Message;
032import org.springframework.messaging.handler.annotation.ValueConstants;
033import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
034import org.springframework.util.ClassUtils;
035
036/**
037 * Abstract base class to resolve method arguments from a named value, e.g.
038 * message headers or destination variables. Named values could have one or more
039 * of a name, a required flag, and a default value.
040 *
041 * <p>Subclasses only need to define specific steps such as how to obtain named
042 * value details from a method parameter, how to resolve to argument values, or
043 * how to handle missing values.
044 *
045 *  <p>A default value string can contain ${...} placeholders and Spring
046 * Expression Language {@code #{...}} expressions which will be resolved if a
047 * {@link ConfigurableBeanFactory} is supplied to the class constructor.
048 *
049 * <p>A {@link ConversionService} is used to convert a resolved String argument
050 * value to the expected target method parameter type.
051 *
052 * @author Rossen Stoyanchev
053 * @author Juergen Hoeller
054 * @since 4.0
055 */
056public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
057
058        private final ConversionService conversionService;
059
060        @Nullable
061        private final ConfigurableBeanFactory configurableBeanFactory;
062
063        @Nullable
064        private final BeanExpressionContext expressionContext;
065
066        private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
067
068
069        /**
070         * Constructor with a {@link ConversionService} and a {@link BeanFactory}.
071         * @param conversionService conversion service for converting String values
072         * to the target method parameter type
073         * @param beanFactory a bean factory for resolving {@code ${...}}
074         * placeholders and {@code #{...}} SpEL expressions in default values
075         */
076        protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionService,
077                        @Nullable ConfigurableBeanFactory beanFactory) {
078
079                // Fallback on shared ConversionService for now for historic reasons.
080                // Possibly remove after discussion in gh-23882.
081
082                //noinspection ConstantConditions
083                this.conversionService = (conversionService != null ?
084                                conversionService : DefaultConversionService.getSharedInstance());
085
086                this.configurableBeanFactory = beanFactory;
087                this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null);
088        }
089
090
091        @Override
092        public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
093                NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
094                MethodParameter nestedParameter = parameter.nestedIfOptional();
095
096                Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
097                if (resolvedName == null) {
098                        throw new IllegalArgumentException(
099                                        "Specified name must not resolve to null: [" + namedValueInfo.name + "]");
100                }
101
102                Object arg = resolveArgumentInternal(nestedParameter, message, resolvedName.toString());
103                if (arg == null) {
104                        if (namedValueInfo.defaultValue != null) {
105                                arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
106                        }
107                        else if (namedValueInfo.required && !nestedParameter.isOptional()) {
108                                handleMissingValue(namedValueInfo.name, nestedParameter, message);
109                        }
110                        arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
111                }
112                else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
113                        arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
114                }
115
116                if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) {
117                        arg = this.conversionService.convert(arg, TypeDescriptor.forObject(arg), new TypeDescriptor(parameter));
118                }
119
120                handleResolvedValue(arg, namedValueInfo.name, parameter, message);
121
122                return arg;
123        }
124
125        /**
126         * Obtain the named value for the given method parameter.
127         */
128        private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
129                NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
130                if (namedValueInfo == null) {
131                        namedValueInfo = createNamedValueInfo(parameter);
132                        namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
133                        this.namedValueInfoCache.put(parameter, namedValueInfo);
134                }
135                return namedValueInfo;
136        }
137
138        /**
139         * Create the {@link NamedValueInfo} object for the given method parameter.
140         * Implementations typically retrieve the method annotation by means of
141         * {@link MethodParameter#getParameterAnnotation(Class)}.
142         * @param parameter the method parameter
143         * @return the named value information
144         */
145        protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
146
147        /**
148         * Fall back on the parameter name from the class file if necessary and
149         * replace {@link ValueConstants#DEFAULT_NONE} with null.
150         */
151        private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
152                String name = info.name;
153                if (info.name.isEmpty()) {
154                        name = parameter.getParameterName();
155                        if (name == null) {
156                                throw new IllegalArgumentException(
157                                                "Name for argument of type [" + parameter.getNestedParameterType().getName() +
158                                                "] not specified, and parameter name information not found in class file either.");
159                        }
160                }
161                return new NamedValueInfo(name, info.required,
162                                ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
163        }
164
165        /**
166         * Resolve the given annotation-specified value,
167         * potentially containing placeholders and expressions.
168         */
169        @Nullable
170        private Object resolveEmbeddedValuesAndExpressions(String value) {
171                if (this.configurableBeanFactory == null || this.expressionContext == null) {
172                        return value;
173                }
174                String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
175                BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
176                if (exprResolver == null) {
177                        return value;
178                }
179                return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
180        }
181
182        /**
183         * Resolves the given parameter type and value name into an argument value.
184         * @param parameter the method parameter to resolve to an argument value
185         * @param message the current request
186         * @param name the name of the value being resolved
187         * @return the resolved argument. May be {@code null}
188         * @throws Exception in case of errors
189         */
190        @Nullable
191        protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name)
192                        throws Exception;
193
194        /**
195         * Invoked when a value is required, but {@link #resolveArgumentInternal}
196         * returned {@code null} and there is no default value. Sub-classes can
197         * throw an appropriate exception for this case.
198         * @param name the name for the value
199         * @param parameter the target method parameter
200         * @param message the message being processed
201         */
202        protected abstract void handleMissingValue(String name, MethodParameter parameter, Message<?> message);
203
204        /**
205         * One last chance to handle a possible null value.
206         * Specifically for booleans method parameters, use {@link Boolean#FALSE}.
207         * Also raise an ISE for primitive types.
208         */
209        @Nullable
210        private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) {
211                if (value == null) {
212                        if (Boolean.TYPE.equals(paramType)) {
213                                return Boolean.FALSE;
214                        }
215                        else if (paramType.isPrimitive()) {
216                                throw new IllegalStateException("Optional " + paramType + " parameter '" + name +
217                                                "' is present but cannot be translated into a null value due to being " +
218                                                "declared as a primitive type. Consider declaring it as object wrapper " +
219                                                "for the corresponding primitive type.");
220                        }
221                }
222                return value;
223        }
224
225        /**
226         * Invoked after a value is resolved.
227         * @param arg the resolved argument value
228         * @param name the argument name
229         * @param parameter the argument parameter type
230         * @param message the message
231         */
232        protected void handleResolvedValue(
233                        @Nullable Object arg, String name, MethodParameter parameter, Message<?> message) {
234        }
235
236
237        /**
238         * Represents a named value declaration.
239         */
240        protected static class NamedValueInfo {
241
242                private final String name;
243
244                private final boolean required;
245
246                @Nullable
247                private final String defaultValue;
248
249                protected NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
250                        this.name = name;
251                        this.required = required;
252                        this.defaultValue = defaultValue;
253                }
254        }
255
256}