3 changed files with 223 additions and 0 deletions
@ -0,0 +1,124 @@
@@ -0,0 +1,124 @@
|
||||
/* |
||||
* Copyright 2002-present 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 org.jspecify.annotations.Nullable; |
||||
import org.springframework.beans.BeanWrapper; |
||||
import org.springframework.beans.PropertyAccessorFactory; |
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.core.convert.ConversionService; |
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.web.bind.annotation.BindParam; |
||||
import org.springframework.web.bind.annotation.ModelAttribute; |
||||
|
||||
import java.beans.PropertyDescriptor; |
||||
import java.lang.reflect.Field; |
||||
import java.util.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* Resolves {@link ModelAttribute}-annotated method parameters by expanding a bean |
||||
* into request parameters for an HTTP client. |
||||
* |
||||
* <p>Behavior: |
||||
* <ul> |
||||
* <li>Each readable bean property yields a request parameter named after the property.</li> |
||||
* <li>{@link BindParam} can override the parameter name. It is supported on both fields and |
||||
* getter methods; if both are present, the getter annotation wins.</li> |
||||
* <li>Null property values are skipped.</li> |
||||
* <li>Values are converted to strings via the configured {@link ConversionService} when |
||||
* possible; otherwise, {@code toString()} is used as a fallback.</li> |
||||
* </ul> |
||||
* |
||||
* @author Hermann Pencole |
||||
* @since 7.0 |
||||
*/ |
||||
public class ModelAttributeArgumentResolver extends AbstractNamedValueArgumentResolver { |
||||
private final ConversionService conversionService; |
||||
|
||||
/** |
||||
* Constructor for a resolver to a String value. |
||||
* @param conversionService the {@link ConversionService} to use to format |
||||
* Object to String values |
||||
*/ |
||||
public ModelAttributeArgumentResolver(ConversionService conversionService) { |
||||
super(); |
||||
this.conversionService = conversionService; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
protected @Nullable NamedValueInfo createNamedValueInfo(MethodParameter parameter) { |
||||
ModelAttribute annot = parameter.getParameterAnnotation(ModelAttribute.class); |
||||
if (annot == null) { |
||||
return null; |
||||
} |
||||
return new NamedValueInfo( |
||||
annot.name(), false, null, "model attribute", |
||||
true); |
||||
} |
||||
|
||||
@Override |
||||
protected void addRequestValue(String name, Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) { |
||||
// Create a map to store custom parameter names
|
||||
Map<String, String> customParamNames = new HashMap<>(); |
||||
|
||||
// Retrieve all @BindParam annotations
|
||||
Class<?> clazz = argument.getClass(); |
||||
for (Field field : clazz.getDeclaredFields()) { |
||||
BindParam bindParam = field.getAnnotation(BindParam.class); |
||||
if (bindParam != null) { |
||||
customParamNames.put(field.getName(), bindParam.value()); |
||||
} |
||||
} |
||||
|
||||
// Convert object to query parameters
|
||||
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(argument); |
||||
for (PropertyDescriptor descriptor : wrapper.getPropertyDescriptors()) { |
||||
String propertyName = descriptor.getName(); |
||||
if (!"class".equals(propertyName)) { |
||||
Object value = wrapper.getPropertyValue(propertyName); |
||||
if (value != null) { |
||||
// Use a custom name if it exists, otherwise use the property name
|
||||
String paramName = customParamNames.getOrDefault(propertyName, propertyName); |
||||
requestValues.addRequestParameter(paramName, convertSingleToString(value)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Convert an arbitrary value to a string using the configured {@link ConversionService} |
||||
* when possible, otherwise falls back to {@code toString()}. |
||||
*/ |
||||
private String convertSingleToString(Object value) { |
||||
try { |
||||
if (this.conversionService.canConvert(value.getClass(), String.class)) { |
||||
String converted = this.conversionService.convert(value, String.class); |
||||
return converted != null ? converted : ""; |
||||
} |
||||
} catch (Exception ignore) { |
||||
// Fallback to toString below
|
||||
} |
||||
return String.valueOf(value); |
||||
} |
||||
|
||||
|
||||
|
||||
} |
||||
@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
|
||||
/* |
||||
* Copyright 2002-present 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 org.junit.jupiter.api.Test; |
||||
import org.springframework.core.convert.support.DefaultConversionService; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.web.bind.annotation.BindParam; |
||||
import org.springframework.web.bind.annotation.ModelAttribute; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.service.annotation.GetExchange; |
||||
import org.springframework.web.service.annotation.PostExchange; |
||||
import org.springframework.web.util.UriComponents; |
||||
import org.springframework.web.util.UriComponentsBuilder; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link ModelAttributeArgumentResolver}. |
||||
* |
||||
* <p>Additional tests for this resolver: |
||||
* <ul> |
||||
* <li>Base class functionality in {@link NamedValueArgumentResolverTests} |
||||
* <li>Form data vs query params in {@link HttpRequestValuesTests} |
||||
* </ul> |
||||
* |
||||
* @author Hermann Pencole |
||||
*/ |
||||
class ModelAttributeArgumentResolverTests { |
||||
|
||||
private final TestExchangeAdapter client = new TestExchangeAdapter(); |
||||
|
||||
private final HttpServiceProxyFactory.Builder builder = HttpServiceProxyFactory.builderFor(this.client); |
||||
|
||||
|
||||
@Test |
||||
void requestParam() { |
||||
Service service = builder.build().createClient(Service.class); |
||||
service.postForm(new MyBean1_2("value 1", true),new MyBean3_4("value 3", List.of("1", "2", "3"))); |
||||
|
||||
Object body = this.client.getRequestValues().getBodyValue(); |
||||
assertThat(body).isInstanceOf(MultiValueMap.class); |
||||
assertThat((MultiValueMap<String, String>) body).hasSize(4) |
||||
.containsEntry("param.1", List.of("value 1")) |
||||
.containsEntry("param2", List.of("true")) |
||||
.containsEntry("param.3", List.of("value 3")) |
||||
.containsEntry("param4", List.of("1,2,3")); |
||||
} |
||||
|
||||
@Test |
||||
void requestParamWithDisabledFormattingCollectionValue() { |
||||
Service service = builder.build().createClient(Service.class); |
||||
service.getWithParams(new MyBean1_2("value 1", true),new MyBean3_4("value 3", List.of("1", "2", "3"))); |
||||
|
||||
HttpRequestValues values = this.client.getRequestValues(); |
||||
String uriTemplate = values.getUriTemplate(); |
||||
Map<String, String> uriVariables = values.getUriVariables(); |
||||
UriComponents uri = UriComponentsBuilder.fromUriString(uriTemplate).buildAndExpand(uriVariables).encode(); |
||||
assertThat(uri.getQuery()).isEqualTo("param.1=value%201¶m2=true¶m.3=value%203¶m4=1,2,3"); |
||||
} |
||||
|
||||
private interface Service { |
||||
|
||||
@PostExchange(contentType = "application/x-www-form-urlencoded") |
||||
void postForm(@ModelAttribute MyBean1_2 param1_2, @ModelAttribute MyBean3_4 param3_4); |
||||
|
||||
@GetExchange |
||||
void getWithParams(@ModelAttribute MyBean1_2 param1_2, @ModelAttribute MyBean3_4 param3_4); |
||||
} |
||||
|
||||
private record MyBean1_2 ( |
||||
@BindParam("param.1") String param1, |
||||
Boolean param2 |
||||
){} |
||||
|
||||
private record MyBean3_4 ( |
||||
@BindParam("param.3") String param3, |
||||
List<String> param4 |
||||
){} |
||||
|
||||
} |
||||
Loading…
Reference in new issue