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.web.method.annotation; 018 019import java.util.Map; 020import java.util.concurrent.ConcurrentHashMap; 021 022import javax.servlet.ServletException; 023 024import org.springframework.beans.ConversionNotSupportedException; 025import org.springframework.beans.TypeMismatchException; 026import org.springframework.beans.factory.config.BeanExpressionContext; 027import org.springframework.beans.factory.config.BeanExpressionResolver; 028import org.springframework.beans.factory.config.ConfigurableBeanFactory; 029import org.springframework.core.MethodParameter; 030import org.springframework.lang.Nullable; 031import org.springframework.web.bind.ServletRequestBindingException; 032import org.springframework.web.bind.WebDataBinder; 033import org.springframework.web.bind.annotation.ValueConstants; 034import org.springframework.web.bind.support.WebDataBinderFactory; 035import org.springframework.web.context.request.NativeWebRequest; 036import org.springframework.web.context.request.RequestScope; 037import org.springframework.web.method.support.HandlerMethodArgumentResolver; 038import org.springframework.web.method.support.ModelAndViewContainer; 039 040/** 041 * Abstract base class for resolving method arguments from a named value. 042 * Request parameters, request headers, and path variables are examples of named 043 * values. Each may have a name, a required flag, and a default value. 044 * 045 * <p>Subclasses define how to do the following: 046 * <ul> 047 * <li>Obtain named value information for a method parameter 048 * <li>Resolve names into argument values 049 * <li>Handle missing argument values when argument values are required 050 * <li>Optionally handle a resolved value 051 * </ul> 052 * 053 * <p>A default value string can contain ${...} placeholders and Spring Expression 054 * Language #{...} expressions. For this to work a 055 * {@link ConfigurableBeanFactory} must be supplied to the class constructor. 056 * 057 * <p>A {@link WebDataBinder} is created to apply type conversion to the resolved 058 * argument value if it doesn't match the method parameter type. 059 * 060 * @author Arjen Poutsma 061 * @author Rossen Stoyanchev 062 * @author Juergen Hoeller 063 * @since 3.1 064 */ 065public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver { 066 067 @Nullable 068 private final ConfigurableBeanFactory configurableBeanFactory; 069 070 @Nullable 071 private final BeanExpressionContext expressionContext; 072 073 private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256); 074 075 076 public AbstractNamedValueMethodArgumentResolver() { 077 this.configurableBeanFactory = null; 078 this.expressionContext = null; 079 } 080 081 /** 082 * Create a new {@link AbstractNamedValueMethodArgumentResolver} instance. 083 * @param beanFactory a bean factory to use for resolving ${...} placeholder 084 * and #{...} SpEL expressions in default values, or {@code null} if default 085 * values are not expected to contain expressions 086 */ 087 public AbstractNamedValueMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory) { 088 this.configurableBeanFactory = beanFactory; 089 this.expressionContext = 090 (beanFactory != null ? new BeanExpressionContext(beanFactory, new RequestScope()) : null); 091 } 092 093 094 @Override 095 @Nullable 096 public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, 097 NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { 098 099 NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); 100 MethodParameter nestedParameter = parameter.nestedIfOptional(); 101 102 Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); 103 if (resolvedName == null) { 104 throw new IllegalArgumentException( 105 "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); 106 } 107 108 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); 109 if (arg == null) { 110 if (namedValueInfo.defaultValue != null) { 111 arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); 112 } 113 else if (namedValueInfo.required && !nestedParameter.isOptional()) { 114 handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); 115 } 116 arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); 117 } 118 else if ("".equals(arg) && namedValueInfo.defaultValue != null) { 119 arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); 120 } 121 122 if (binderFactory != null) { 123 WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); 124 try { 125 arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); 126 } 127 catch (ConversionNotSupportedException ex) { 128 throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), 129 namedValueInfo.name, parameter, ex.getCause()); 130 } 131 catch (TypeMismatchException ex) { 132 throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(), 133 namedValueInfo.name, parameter, ex.getCause()); 134 } 135 } 136 137 handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); 138 139 return arg; 140 } 141 142 /** 143 * Obtain the named value for the given method parameter. 144 */ 145 private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { 146 NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); 147 if (namedValueInfo == null) { 148 namedValueInfo = createNamedValueInfo(parameter); 149 namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); 150 this.namedValueInfoCache.put(parameter, namedValueInfo); 151 } 152 return namedValueInfo; 153 } 154 155 /** 156 * Create the {@link NamedValueInfo} object for the given method parameter. Implementations typically 157 * retrieve the method annotation by means of {@link MethodParameter#getParameterAnnotation(Class)}. 158 * @param parameter the method parameter 159 * @return the named value information 160 */ 161 protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); 162 163 /** 164 * Create a new NamedValueInfo based on the given NamedValueInfo with sanitized values. 165 */ 166 private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { 167 String name = info.name; 168 if (info.name.isEmpty()) { 169 name = parameter.getParameterName(); 170 if (name == null) { 171 throw new IllegalArgumentException( 172 "Name for argument of type [" + parameter.getNestedParameterType().getName() + 173 "] not specified, and parameter name information not found in class file either."); 174 } 175 } 176 String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); 177 return new NamedValueInfo(name, info.required, defaultValue); 178 } 179 180 /** 181 * Resolve the given annotation-specified value, 182 * potentially containing placeholders and expressions. 183 */ 184 @Nullable 185 private Object resolveEmbeddedValuesAndExpressions(String value) { 186 if (this.configurableBeanFactory == null || this.expressionContext == null) { 187 return value; 188 } 189 String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); 190 BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver(); 191 if (exprResolver == null) { 192 return value; 193 } 194 return exprResolver.evaluate(placeholdersResolved, this.expressionContext); 195 } 196 197 /** 198 * Resolve the given parameter type and value name into an argument value. 199 * @param name the name of the value being resolved 200 * @param parameter the method parameter to resolve to an argument value 201 * (pre-nested in case of a {@link java.util.Optional} declaration) 202 * @param request the current request 203 * @return the resolved argument (may be {@code null}) 204 * @throws Exception in case of errors 205 */ 206 @Nullable 207 protected abstract Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) 208 throws Exception; 209 210 /** 211 * Invoked when a named value is required, but {@link #resolveName(String, MethodParameter, NativeWebRequest)} 212 * returned {@code null} and there is no default value. Subclasses typically throw an exception in this case. 213 * @param name the name for the value 214 * @param parameter the method parameter 215 * @param request the current request 216 * @since 4.3 217 */ 218 protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request) 219 throws Exception { 220 221 handleMissingValue(name, parameter); 222 } 223 224 /** 225 * Invoked when a named value is required, but {@link #resolveName(String, MethodParameter, NativeWebRequest)} 226 * returned {@code null} and there is no default value. Subclasses typically throw an exception in this case. 227 * @param name the name for the value 228 * @param parameter the method parameter 229 */ 230 protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { 231 throw new ServletRequestBindingException("Missing argument '" + name + 232 "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); 233 } 234 235 /** 236 * A {@code null} results in a {@code false} value for {@code boolean}s or an exception for other primitives. 237 */ 238 @Nullable 239 private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) { 240 if (value == null) { 241 if (Boolean.TYPE.equals(paramType)) { 242 return Boolean.FALSE; 243 } 244 else if (paramType.isPrimitive()) { 245 throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name + 246 "' is present but cannot be translated into a null value due to being declared as a " + 247 "primitive type. Consider declaring it as object wrapper for the corresponding primitive type."); 248 } 249 } 250 return value; 251 } 252 253 /** 254 * Invoked after a value is resolved. 255 * @param arg the resolved argument value 256 * @param name the argument name 257 * @param parameter the argument parameter type 258 * @param mavContainer the {@link ModelAndViewContainer} (may be {@code null}) 259 * @param webRequest the current request 260 */ 261 protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter, 262 @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { 263 } 264 265 266 /** 267 * Represents the information about a named value, including name, whether it's required and a default value. 268 */ 269 protected static class NamedValueInfo { 270 271 private final String name; 272 273 private final boolean required; 274 275 @Nullable 276 private final String defaultValue; 277 278 public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) { 279 this.name = name; 280 this.required = required; 281 this.defaultValue = defaultValue; 282 } 283 } 284 285}