Spring MVC. Default values for fields of method parameter - java

I have a simple controller with method test:
#RequestMapping(produces = "application/json")
#ResponseBody
public HttpEntity<Void> test(Test test) {
return new ResponseEntity<>(HttpStatus.OK);
}
Test class looks like this:
public class Test {
private String name;
private Date date;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getDate() {
return date;
}
#DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
public void setDate(Date date) {
this.date = date;
}
}
And I need default values for fields of Test object. If I had a primitive param, I would be able to use #RequestParam(required = false, defaultValue = "someValue"). But with non-primitive param this approach doesn't seem to work. I see a couple of variants how to deal with it:
Assign values in a constructor. Not very good, because may be I will
need different defaults for different methods.
Write custom DataBinder. Better, but the problem with different
defaults still exists.
Write custom DataBinder and custom annotation with defaults.
Am I missing something and there is a built in feature which can solve my problem?

You can lean on argument resolving, in four easy steps, similiar as you suggested in your third point.
Create an annotation e.g.
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface TestDefaultValues {
String[] value();
}
Write a resolver e.g.
public class TestArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(TestDefaultValues.class) != null;
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
TestDefaultValues attr = parameter.getParameterAnnotation(TestDefaultValues.class);
String[] value = attr.value();
Test test = new Test();
test.setName(value[0]);
test.setDate(new Date(value[1]));
return test;
}
}
register a resolver
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="your.package.TestArgumentResolver"></bean>
</mvc:argument-resolvers>
</mvc:annotation-driven>
use an annotation in your controller method e.g.
public HttpEntity<Void> test(#TestDefaultValues({"foo","11/12/2014"}) Test test) {
instantiating date is just to get the gist of the implementation, obviously you'll use whatever is your idea

Related

Convert #RequestParam to custom Object

Have a problem with optimizing search request.
I have search method that accepts parameters in url query like:
http://localhost:8080/api?code.<type>=<value>&name=Test
Example: http://localhost:8080/api?code.phone=9999999999&name=Test
Defined SearchDto:
public class SearchDto {
String name;
List<Code> code;
}
Defined Code class:
public class Code {
String type;
String value;
}
Currently I'm using Map<String,String> as incoming parameter for the method:
#GetMapping("/search")
public ResponseEntity<?> search(final #RequestParam Map<String, String> searchParams) {
return service.search(searchParams);
}
Then manually converting map values for SearchDto class. Is it possible to get rid of Map<String,String> and pass SearchDto directly as argument in controller method?
Passing a json in querystring is actually a bad practice, since it decrease the security and sets limits on the number of parameters you can send to your endpoint.
Technically speaking, you could make everything work by using your DTO as a controller's parameter, then URL encoding the json before you send it to the backend.
The best option, in your case, is to serve an endpoint that listen to a POST request: it is not an error, neither a bad practise, to use POST when performing a search.
you can customize a HandlerMethodArgumentResolver to implement it.
but , if you want a object receive incoming parameter. why not use POST
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Example {
}
public class ExampleArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
Example requestParam = parameter.getParameterAnnotation(Example.class);
return requestParam != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, #Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, #Nullable WebDataBinderFactory binderFactory) throws Exception {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
Map<String, String[]> parameterMap = webRequest.getParameterMap();
Map<String, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
result.put(key, values[0]);
}
});
//here will return a map object. then you convert map to your object, I don't know how to convert , but you have achieve it.
return o;
}
}
add to container
#Configuration
#EnableWebMvc
public class ExampleMvcConfiguration implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new ExampleArgumentResolver());
}
}
usage
#RestController
public class TestCtrl {
#GetMapping("api")
public Object gg(#Example SearchDto searchDto) {
System.out.println(searchDto);
return "1";
}
#Data
public static class SearchDto {
String name;
List<Code> code;
}
#Data
public static class Code {
String type;
String value;
}
}
Here is a demo.

Convert List of Enums to List of String for Spring #RequestParam using feign client

I have an enum class as such:
ONE("1", "Description1"),
TWO("2", "Description2");
String value;
String description;
MyEnum(String value, String description) {
this.value = value;
this.description = description;
}
#Override
public String toString() {
return this.value;
}
#JsonValue
public String value() {
return this.value;
}
The API I am interacting with is expecting a param with type String and the values can be comma separated.
For example: api.com/test?param1=1,2
I configured a feign client with the url api.com/test
And then created a POJO like so
public class POJO {
private List<MyEnum> param1;
}
And in my feign client I have:
#RequestMapping(method = RequestMethod.GET)
MyResponse getResponse(#SpringQueryMap POJO request);
Is it possible to somehow turn the List of Enums to a List of String before the API call is made via some Spring approach?
As of right now, when I pass a List of Enums, it is only taking into account the last Enum within this list.
UPDATE: I annotated the property I want to convert to a list using #JsonSerialize(converter=abc.class). However #SpringQueryMap doesn't seem to honor that serialization..
Yes is possible, you need to create an interceptor and in that method do the mapping.
This topic may be for you.
Spring - Execute code before controller's method is invoked
So turns out #JsonSerialize was not working with #SpringQueryMap
So I did have to add an interceptor.
Like so:
public class MyInterceptor implements RequestInterceptor {
#Override
public void apply(RequestTemplate requestTemplate) {
if(requestTemplate.queries().containsKey("param1")) {
requestTemplate.query("param1", convert(requestTemplate.queries().get("param1")));
}
}
//convert list to a string
public String convert(Collection<String> values) {
final String s = String.join(",", values.stream().map(Object::toString).collect(Collectors.toList()));
return s;
}
}
And then in my Feign config class added this:
#Bean
public MyInterceptor myInterceptor() {
return new MyInterceptor();
}

How to use Spring Converter for some controllers only?

I have c converter which works:
public class StringToLongConverter implements Converter<String, Long> {
#Override
public Long convert(String source) {
Long myDecodedValue = ...
return myDecodedValue;
}
}
In web configuration I have:
#Override
public void addFormatters (FormatterRegistry registry) {
registry.addConverter(new StringToLongConverter());
}
Everything is good but it works for all controllers and I need it to be executed only for some controllers.
//I need this controller to get myvalue from converter
#RequestMapping(value = "{myvalue}", method = RequestMethod.POST)
public ResponseEntity myvalue1(#PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
//I need this controller to get myvalue without converter
#RequestMapping(value = "{myvalue}", method = RequestMethod.POST)
public ResponseEntity myvalue2(#PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
Can we specify which converters or parameters should be used with custom converter and which should not?
Normally speaking, a registered Converter is bound to an input source and an output destination. In your case <String, Long>. The default Spring converter you used will apply the conversion on each matching source-destination pair.
To gain more control over when to conditionally apply the conversion, a ConditionalGenericConverter can be used. The interface contains 3 methods:
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType), to determine whether the conversion should be applied
Set<ConvertiblePair> getConvertibleTypes() to return a set of source-destination pairs the conversion can be applied to
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) the method in which the actual conversion takes places.
I've set up a small Spring project to play around with the use of a ConditionalGenericConverter:
RequiresConversion.java:
// RequiresConversion is a custom annotation solely used in this example
// to annotate an attribute as "convertable"
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface RequiresConversion {
}
SomeConverter.java:
#Component
public class SomeConverter implements ConditionalGenericConverter {
#Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Verify whether the annotation is present
return targetType.getAnnotation(RequiresConversion.class) != null;
}
#Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, Long.class));
}
#Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
// Conversion logic here
// In this example it strips "value" from the source string
String sourceValue = ((String) source).replace("value", "");
return Long.valueOf(sourceValue);
}
}
SomeController.java:
#RestController
public class SomeController {
// The path variable used will be converted, resulting in the "value"-prefix
// being stripped in SomeConverter
// Notice the custom '#RequiresConversion' annotation
#GetMapping(value = "/test/{myvalue}")
public ResponseEntity myvalue(#RequiresConversion #PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
// As the #RequiresConversion annotation is not present,
// the conversion is not applied to the #PathVariable
#GetMapping(value = "/test2/{myvalue}")
public ResponseEntity myvalue2(#PathVariable Long myvalue) {
return new ResponseEntity<>(HttpStatus.OK);
}
}
The conversion will occur on http://localhost:8080/test/value123 , resulting in a 123 Long value. However, as the custom annotation #RequiresConversion is not present on the second mapping, the conversion on http://localhost:8080/test2/value123 will be skipped.
You could also inverse the annotation by renaming it to SkipConversion and verifying whether the annotation is absent in the matches() method.
Hope this helps!

Custom annotation isn't validating method parameter

I created a custom annotation
#Documented
#Constraint(validatedBy = CheckGranularityValidator.class)
#Target( { ElementType.PARAMETER} )
#Retention(RetentionPolicy.RUNTIME)
public #interface CheckGranularity {
String message() default "Duration has to be a multiple of granularity";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
With a validator like so
public class CheckGranularityValidator implements ConstraintValidator<CheckGranularity, AssetCostsRequest> {
#Override
public void initialize(final CheckGranularity constraintAnnotation) {
}
#Override
public boolean isValid(final AssetCostsRequest value, final ConstraintValidatorContext context) {
return value.getRange().getDuration() % value.getGranularity() == 0;
}
}
I tried using it in my RestController
#RestController
public class CalcApiController extends CalcApi {
#Override
public ResponseEntity<String> calcProfitability(#Valid #CheckGranularity #RequestBody final AssetCostsRequest assetCostsRequest) {
return ResponseEntity.ok("Works");
}
I tried using this annotation by writing a test:
#Test
public void calcTest() {
final AssetCostsRequest request = new AssetCostsRequest()
.setRange(new TimeRange(100L, 200L))
.setGranularity(26L);
given()
.contentType(ContentType.JSON)
.body(request)
.when()
.post("/calc")
.then()
.statusCode(HttpStatus.SC_BAD_REQUEST);
}
Relevant part of AssetCostsRequest:
public class AssetCostsRequest {
#JsonProperty
#NotNull
private TimeRange range;
#JsonProperty
#NotNull
private Long granularity = 30L;
...getters & setters
}
Test method returns with 200. When I try to set a breakpoint in isValid method, it isn't hit when I run the test. I tried changing order of annotations, getting rid of #Valid, changing #Target in CheckGranularity class, nothing helped. I'm using RestAssured for testing.
How do I make it, so my annotation is properly validating a parameter?
Change CheckGranularity's target to ElementType.TYPE and add #CheckGranularity directly on AssetCostsRequest. Also remove #CheckGranularity from endpoint definition.
How it works. By adding #Valid on endpoint's parameter you tell spring to validate it. Adding validation like #CheckGranularity won't work on the same level as Valid. It has to be added somewhere inside parameters class.

How to validate Integer or long in spring mvc

class employee{
...
private long phone;
...
}
I want to validate phone number using spring jsr303 validator, In my Controller I am using #valid. I am successfully validating entered value is number or string by using generic typeMismatch placing in error message property file.
But I want to validate entered number format is correct or not.(#pattern for string only)
How to achieve this one,please suggest me.
Normally phone numbers are String and you can validate by using #Pattern, but if you want to validate any fields you can do like this.
Custom annotation Javax validator
#javax.validation.Constraint(validatedBy = { PhoneNumberConstraintValidator.class })
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
#Retention(RUNTIME)
public #interface ValidPhoneNumber {
}
public class PhoneNumberConstraintValidator implements ConstraintValidator<ValidPhoneNumber, Long> {
#Override
public void initialize(final ValidPhoneNumber constraintAnnotation) {
// nop
}
#Override
public boolean isValid(final Long value, final ConstraintValidatorContext context) {
//your custom validation logic
}
}
class employee{
...
private long phone;
#ValidPhoneNumber
public Long getPhone() { return phone; }
...
}
OR simpler if you have hibernate validator, you can just add this method in your entity class.
#org.hibernate.validator.AssertTrue
public boolean validatePhoneNumber() { }

Categories

Resources