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