Extract base class AbstractNamedValueArgumentResolver

Closes gh-28395
This commit is contained in:
rstoyanchev
2022-04-29 17:34:16 +01:00
parent d91b840d0e
commit f8ac5985bd
6 changed files with 382 additions and 300 deletions

View File

@@ -0,0 +1,235 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.service.invoker;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.ValueConstants;
/**
* Base class for arguments that resolve to a named request value such as a
* request header, path variable, cookie, and others.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public abstract class AbstractNamedValueArgumentResolver implements HttpServiceArgumentResolver {
protected final Log logger = LogFactory.getLog(getClass());
private final ConversionService conversionService;
private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
/**
* Create an instance.
* @param conversionService the {@link ConversionService} to use to format
* Object to String values.
*/
protected AbstractNamedValueArgumentResolver(ConversionService conversionService) {
Assert.notNull(conversionService, "ConversionService is required");
this.conversionService = conversionService;
}
/**
* Return the configured {@link ConversionService}.
*/
public ConversionService getConversionService() {
return this.conversionService;
}
@SuppressWarnings("unchecked")
@Override
public boolean resolve(
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
NamedValueInfo info = getNamedValueInfo(parameter);
if (info == null) {
return false;
}
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
Assert.isInstanceOf(Map.class, argument);
for (Map.Entry<String, ?> entry : ((Map<String, ?>) argument).entrySet()) {
addSingleOrMultipleValues(
entry.getKey(), entry.getValue(), false, null, info.label, info.multiValued,
requestValues);
}
}
else {
addSingleOrMultipleValues(
info.name, argument, info.required, info.defaultValue, info.label, info.multiValued,
requestValues);
}
return true;
}
@Nullable
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
NamedValueInfo info = this.namedValueInfoCache.get(parameter);
if (info == null) {
info = createNamedValueInfo(parameter);
if (info == null) {
return null;
}
info = updateNamedValueInfo(parameter, info);
this.namedValueInfoCache.put(parameter, info);
}
return info;
}
/**
* Return information about the request value, or {@code null} if the
* parameter does not represent a request value of interest.
*/
@Nullable
protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
String name = info.name;
if (info.name.isEmpty()) {
name = parameter.getParameterName();
if (name == null) {
throw new IllegalArgumentException(
"Name for argument of type [" + parameter.getParameterType().getName() + "] " +
"not specified, and parameter name information not found in class file either.");
}
}
boolean required = (info.required && !parameter.getParameterType().equals(Optional.class));
String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
return info.update(name, required, defaultValue);
}
private void addSingleOrMultipleValues(
String name, @Nullable Object value, boolean required, @Nullable Object defaultValue,
String valueLabel, boolean supportsMultiValues, HttpRequestValues.Builder requestValues) {
if (supportsMultiValues) {
value = (ObjectUtils.isArray(value) ? Arrays.asList((Object[]) value) : value);
if (value instanceof Collection<?> elements) {
boolean hasValues = false;
for (Object element : elements) {
if (element != null) {
hasValues = true;
addSingleValue(name, element, false, null, valueLabel, requestValues);
}
}
if (hasValues) {
return;
}
value = null;
}
}
addSingleValue(name, value, required, defaultValue, valueLabel, requestValues);
}
private void addSingleValue(
String name, @Nullable Object value, boolean required, @Nullable Object defaultValue, String valueLabel,
HttpRequestValues.Builder requestValues) {
if (value instanceof Optional<?> optionalValue) {
value = optionalValue.orElse(null);
}
if (value == null && defaultValue != null) {
value = defaultValue;
}
if (!(value instanceof String)) {
value = getConversionService().convert(value, String.class);
}
if (value == null) {
Assert.isTrue(!required, "Missing " + valueLabel + " value '" + name + "'");
return;
}
if (logger.isTraceEnabled()) {
logger.trace("Resolved " + valueLabel + " value '" + name + ":" + value + "'");
}
addRequestValue(name, (String) value, requestValues);
}
/**
* Add the given, single request value. This may be called multiples times
* if the request value is multivalued.
* @param name the request value name
* @param value the value
* @param requestValues builder to add the request value to
*/
protected abstract void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues);
/**
* Info about a request value, typically extracted from a method parameter annotation.
*/
protected static class NamedValueInfo {
private final String name;
private final boolean required;
@Nullable
private final String defaultValue;
private final String label;
private final boolean multiValued;
/**
* Create an instance.
* @param name the name to use, possibly empty if not specified
* @param required whether it is marked as required
* @param defaultValue fallback value, possibly {@link ValueConstants#DEFAULT_NONE}
* @param label how it should appear in error messages, e.g. "path variable", "request header"
*/
public NamedValueInfo(
String name, boolean required, @Nullable String defaultValue, String label, boolean multiValued) {
this.name = name;
this.required = required;
this.defaultValue = defaultValue;
this.label = label;
this.multiValued = multiValued;
}
public NamedValueInfo update(String name, boolean required, @Nullable String defaultValue) {
return new NamedValueInfo(name, required, defaultValue, this.label, this.multiValued);
}
}
}

View File

@@ -34,6 +34,7 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
@@ -86,7 +87,7 @@ final class HttpServiceMethod {
DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
MethodParameter[] parameters = new MethodParameter[count];
for (int i = 0; i < count; i++) {
parameters[i] = new MethodParameter(method, i);
parameters[i] = new SynthesizingMethodParameter(method, i);
parameters[i].initParameterNameDiscovery(nameDiscoverer);
}
return parameters;

View File

@@ -16,94 +16,48 @@
package org.springframework.web.service.invoker;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
/**
* An implementation of {@link HttpServiceArgumentResolver} that resolves
* request path variables based on method arguments annotated
* with {@link PathVariable}. {@code null} values are allowed only
* if {@link PathVariable#required()} is {@code true}.
* {@link HttpServiceArgumentResolver} for {@link PathVariable @PathVariable}
* annotated arguments.
*
* <p>The argument may be a single variable value or a {@code Map} with multiple
* variable and values. Each value may be a String or an Object to be converted
* to a String through the configured {@link ConversionService}.
*
* <p>If the value is required but {@code null}, {@link IllegalArgumentException}
* is raised. The value is not required if:
* <ul>
* <li>{@link PathVariable#required()} is set to {@code false}
* <li>The argument is declared as {@link java.util.Optional}
* </ul>
*
* @author Olga Maciaszek-Sharma
* @author Rossen Stoyanchev
* @since 6.0
*/
public class PathVariableArgumentResolver implements HttpServiceArgumentResolver {
private static final Log logger = LogFactory.getLog(PathVariableArgumentResolver.class);
private final ConversionService conversionService;
public class PathVariableArgumentResolver extends AbstractNamedValueArgumentResolver {
public PathVariableArgumentResolver(ConversionService conversionService) {
Assert.notNull(conversionService, "ConversionService is required");
this.conversionService = conversionService;
super(conversionService);
}
@SuppressWarnings("unchecked")
@Override
public boolean resolve(
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class);
if (annotation == null) {
return false;
}
Class<?> parameterType = parameter.getParameterType();
boolean required = (annotation.required() && !Optional.class.isAssignableFrom(parameterType));
if (Map.class.isAssignableFrom(parameterType)) {
if (argument != null) {
Assert.isInstanceOf(Map.class, argument);
((Map<String, ?>) argument).forEach((key, value) ->
addUriParameter(key, value, required, requestValues));
}
}
else {
String name = StringUtils.hasText(annotation.value()) ? annotation.value() : annotation.name();
name = StringUtils.hasText(name) ? name : parameter.getParameterName();
Assert.notNull(name, "Failed to determine path variable name for parameter: " + parameter);
addUriParameter(name, argument, required, requestValues);
}
return true;
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
PathVariable annot = parameter.getParameterAnnotation(PathVariable.class);
return (annot == null ? null :
new NamedValueInfo(annot.name(), annot.required(), null, "path variable", false));
}
private void addUriParameter(
String name, @Nullable Object value, boolean required, HttpRequestValues.Builder requestValues) {
if (value instanceof Optional) {
value = ((Optional<?>) value).orElse(null);
}
if (!(value instanceof String)) {
value = this.conversionService.convert(value, String.class);
}
if (value == null) {
Assert.isTrue(!required, "Missing required path variable '" + name + "'");
return;
}
if (logger.isTraceEnabled()) {
logger.trace("Resolved path variable '" + name + "' to " + value);
}
requestValues.setUriVariable(name, (String) value);
@Override
protected void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues) {
requestValues.setUriVariable(name, value);
}
}

View File

@@ -16,22 +16,9 @@
package org.springframework.web.service.invoker;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ValueConstants;
/**
@@ -49,100 +36,36 @@ import org.springframework.web.bind.annotation.ValueConstants;
* <p>Individual header values may be Strings or Objects to be converted to
* String values through the configured {@link ConversionService}.
*
* <p>If the value is required but {@code null}, {@link IllegalArgumentException}
* is raised. The value is not required if:
* <ul>
* <li>{@link RequestHeader#required()} is set to {@code false}
* <li>{@link RequestHeader#defaultValue()} provides a fallback value
* <li>The argument is declared as {@link java.util.Optional}
* </ul>
*
* @author Olga Maciaszek-Sharma
* @author Rossen Stoyanchev
* @since 6.0
*/
public class RequestHeaderArgumentResolver implements HttpServiceArgumentResolver {
private static final Log logger = LogFactory.getLog(RequestHeaderArgumentResolver.class);
private final ConversionService conversionService;
public class RequestHeaderArgumentResolver extends AbstractNamedValueArgumentResolver {
public RequestHeaderArgumentResolver(ConversionService conversionService) {
Assert.notNull(conversionService, "ConversionService is required");
this.conversionService = conversionService;
super(conversionService);
}
@SuppressWarnings("unchecked")
@Override
public boolean resolve(
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
RequestHeader annot = parameter.getParameterAnnotation(RequestHeader.class);
if (annot == null) {
return false;
}
Class<?> parameterType = parameter.getParameterType();
boolean required = (annot.required() && !Optional.class.isAssignableFrom(parameterType));
Object defaultValue = (ValueConstants.DEFAULT_NONE.equals(annot.defaultValue()) ? null : annot.defaultValue());
if (Map.class.isAssignableFrom(parameterType)) {
if (argument != null) {
Assert.isInstanceOf(Map.class, argument);
((Map<String, ?>) argument).forEach((key, value) ->
addHeader(key, value, false, defaultValue, requestValues));
}
}
else {
String name = StringUtils.hasText(annot.value()) ? annot.value() : annot.name();
name = StringUtils.hasText(name) ? name : parameter.getParameterName();
Assert.notNull(name, "Failed to determine request header name for parameter: " + parameter);
addHeader(name, argument, required, defaultValue, requestValues);
}
return true;
return (annot == null ? null :
new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(), "request header", true));
}
private void addHeader(
String name, @Nullable Object value, boolean required, @Nullable Object defaultValue,
HttpRequestValues.Builder requestValues) {
value = (ObjectUtils.isArray(value) ? Arrays.asList((Object[]) value) : value);
if (value instanceof Collection<?> elements) {
boolean hasValue = false;
for (Object element : elements) {
if (element != null) {
hasValue = true;
addHeaderValue(name, element, false, requestValues);
}
}
if (hasValue) {
return;
}
value = null;
}
if (value instanceof Optional<?> optionalValue) {
value = optionalValue.orElse(null);
}
if (value == null && defaultValue != null) {
value = defaultValue;
}
addHeaderValue(name, value, required, requestValues);
}
private void addHeaderValue(
String name, @Nullable Object value, boolean required, HttpRequestValues.Builder requestValues) {
if (!(value instanceof String)) {
value = this.conversionService.convert(value, String.class);
}
if (value == null) {
Assert.isTrue(!required, "Missing required header '" + name + "'");
return;
}
if (logger.isTraceEnabled()) {
logger.trace("Resolved header '" + name + ":" + value + "'");
}
requestValues.addHeader(name, (String) value);
@Override
protected void addRequestValue(String name, String value, HttpRequestValues.Builder requestValues) {
requestValues.addHeader(name, value);
}
}