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