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