Spring Boot/Java Mapping Enum Values to RequestParam [duplicate] - spring-boot

This question already has answers here:
Spring's #RequestParam with Enum
(9 answers)
Closed 3 years ago.
I have an Enum like below
public enum Customer {
RETAIL("retail"),
FREELANCER("FreeLancer"),
MARKET("market"),
PUBLICATION("publication");
private String contentType;
private static final Map<String,Customer> contentTypeMap;
public String getContentType(){
return this.contentType;
}
static {
Map<String,Customer> map = new ConcurrentHashMap<>();
for(Customer type : Customer.values ()){
map.put (type.getContentType (),type);
}
contentTypeMap = map;
}
Customer(String contentType){
this.contentType=contentType;
}
public static Customer getContentType(String contentType){
return contentTypeMap.get (contentType);
}
}
This enum represents the type of customer.
We have an API that return the customer details
#RequestMapping(value="/getData", method=RequestMethod.GET, produces="application/json")
public BatchResponse triggerBatchJob(
#RequestParam(value="updateFrom", required=false) #DateTimeFormat(pattern="yyyyMMdd") String updateFrom,
#RequestParam(value="updateTo", required=false) #DateTimeFormat(pattern="yyyyMMdd") String updateTo,
#RequestParam(value="customerType") (VALIDATE_HERE)String customerType) {
// ...
}
I need to validate the customerType value to be the ones present in the Enum, Is there a way to validate the same with the method declaration as I have done in the case of date rather than method body by using getContentType method or something.
Please help.

Change your method to following:
#RequestMapping(value="/getData", method=RequestMethod.GET, produces="application/json")
public BatchResponse triggerBatchJob(
#RequestParam(value="updateFrom", required=false) #DateTimeFormat(pattern="yyyyMMdd") String updateFrom,
#RequestParam(value="updateTo", required=false) #DateTimeFormat(pattern="yyyyMMdd") String updateTo,
#RequestParam(value="customerType") CustomerType customerType) {
// ...
}
i.e. customerType type should be CustomerType not String. Now only values those match enum will be mapped.
Note:- The values will have to be provided is specific format i.e. enum name itself e.g. in your case FREELANCER,RETAIL, PUBLICATION etc values should be passed in request.
Edit
As requested by OP below is customizing the enum handling from String:
Add #initBinder in the controller and add following method:
#InitBinder
public void initBinder(final WebDataBinder webdataBinder) {
webdataBinder.registerCustomEditor(Customer.class, new CustomerConverter());
}
and declare a converter class as below:
import java.beans.PropertyEditorSupport;
public class CustomerConverter extends PropertyEditorSupport{
public void setAsText(final String text) throws IllegalArgumentException {
System.out.println("--->"+Customer.getContentType(text));
setValue(Customer.getContentType(text));
}¡¡
}
Added System.out.println to show that value is interpreted and printed as expected.

A simple null check will do
Customer customer = Customer.getContentType(customerType);
if (customer == null) {
throw new Exception("Invalid Customer type");// use validation exception
}

Related

How to create a custom annotated validation that has a param in spring boot? [duplicate]

This question already has an answer here:
How to use an annotation element inside a custom constraint validator
(1 answer)
Closed 4 months ago.
Is it possible to pass a value when using a custom annotated validation? The logic is different depending on the param value. In the example below, the chill room may require the key-value pairs to include "snack" : "" with max length 10, min length 1 similar to the #Size(min = 1, max = 10). I'm implementing the ConstraintValidator and set up the interface.
i.e.
#ConcertValidation(dressingRoom = "chill")
private List<Map<String, String>> json;
Inside the initialize method of the ConstraintValidator implementation, you can capture the value from annotation. For example:
class ConcertValidator implements ConstraintValidator<ConcertValidation, List<Map<String, String>>> {
String dressingRoom;
#Override
public void initialize(ConcertValidation constraintAnnotation) {
dressingRoom = constraintAnnotation.dressingRoom();
}
#Override
public boolean isValid(List<Map<String, String>> value, ConstraintValidatorContext context) {
boolean isValid = false;
//TODO implement your logic depending on dressingRoom value
return isValid;
}
}
In the interface for the validator, include:
//default code
pubilc #interface ConcertValidation{
// default code
String dressingRoom() default "";
}
In the validator class, you must initialize the params you want to pass to use them in your logic for the isValid() method;
// global variables
private String dressingRoom;
#Override
public void initialize(ConcertValidation concertValidationConstraint){
this.dressingRoom = concertValidationConstraint.dressingRoom();
}
Whenever you use the custom annotation validation, just pass the params.
#ConcertValidation(dressingRoom = "chill")
private List<Map<String, String>> json;

FormUrlEncoded POST request, I need to convert snake case values into camelCase with SpringBoot and Jackson

I am integrating with a third-party's vendor API.
I have a SpringBoot and Jackson setup
They are sending me a POST request that is of type formUrlEncoded and with the params in snake_case
(over 10 params in total and no body)
e.g.
POST www.example.com?player_id=somePlayerId&product_id=someProductId&total_amount=totalAmount...
There are many out of the box helpers for JSON but I cannot find any for formUrlEncoded (I hope I am missing something obvious).
I have tried #ModelAttribute and #RequestParam but had no luck.
I am trying to avoid the #RequestParam MultiValueMap<String, String> params + custom mapper option
#RequestParam is the simplest way which allows you to define the exact name of the query parameter something like:
#PostMapping
public String foo(#RequestParam("player_id") String playerId){
}
If you want to bind all the query parameters to an object , you have to use #ModelAttribute. It is based on the DataBinder and is nothing to do with Jackson. By default it only supports binding the query parameter to an object which fields have the same name as the query parameter. So you can consider to bind the query paramater to the following object :
public class Request {
private String player_id;
private String product_id;
private Long total_amount;
}
If you really want to bind to the object that follow traditional java naming convention (i.e lower camel case) from the query parameter that has snake case values , you have to cusomtize WebDataBinder.
The idea is to override its addBindValues() and check if the query parameter name is in snake case format , convert it the lower camel case format and also add it as the bind values for the request. Something like :
public class MyServletRequestDataBinder extends ExtendedServletRequestDataBinder {
private static Converter<String, String> snakeCaseToLowerCamelConverter = CaseFormat.LOWER_UNDERSCORE
.converterTo(CaseFormat.LOWER_CAMEL);
public MyServletRequestDataBinder(Object target) {
super(target);
}
public MyServletRequestDataBinder(Object target, String objectName) {
super(target, objectName);
}
#Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames != null && paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
if(paramName.contains("_")) {
String[] values = request.getParameterValues(paramName);
if (values == null || values.length == 0) {
// Do nothing, no values found at all.
} else if (values.length > 1) {
mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values);
} else {
mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values[0]);
}
}
}
}
}
P.S I am using Guava for helping me to convert snake case to lowerCamelCase.
But in order to use the customized WebDataBinder , you have to in turn customize WebDataBinderFactory and RequestMappingHandlerAdapter because :
customize WebDataBinderFactory in order to create the customised WebDataBinder
customize RequestMappingHandlerAdapter in order to create the WebDataBinderFactory
Something like:
public class MyServletRequestDataBinderFactory extends ServletRequestDataBinderFactory {
public MyServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods,
WebBindingInitializer initializer) {
super(binderMethods, initializer);
}
#Override
protected ServletRequestDataBinder createBinderInstance(Object target, String objectName,
NativeWebRequest request) throws Exception {
return new MyServletRequestDataBinder(target, objectName);
}
}
and
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
#Override
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
throws Exception {
return new MyServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}
}
And finally register to use the customised RequestMappingHandlerAdapter in your configuration :
#Configuration
public class Config extends DelegatingWebMvcConfiguration {
#Override
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}
}
I don't think you are missing anything. Looking at the RequestParamMethodArgumentResolver#resolveName source I do no see a way to customize how a request parameter is matched. So it looks either you have to implement your own resolver or just annotate each parameter with #RequestParam and provide the name, e.g. #RequestParam("product_id") String productId
EDIT:
As for ModelAttribute, ModelAttributeMethodProcessor uses WebDataBinder. Again you can customize it with your custom DataBinder but I didn't found any that out of the box supports aliases as Jackson does.

Spring MVC: Refusing matched mapping

Consider a situation where we can have several mappings with the same regular expression, which should be validated programmatically (for instance against database).
(this is not a valid piece of code, I am trying just to explain what I am trying to achieve. Note the regular expressions in the url path)
// Animal controller
#GetMapping(path = "/{animal-category [a-z-]+}/{animal-name [a-z-]+}")
public void show(#PathVariable String animalCategory, #PathVariable String animalName) {
// if animalCategory is not found in database, continue with next controller
}
// Plants controller
#GetMapping(path = "/{plant-category [a-z-]+}/{plant-name [a-z-]+}")
public void show(#PathVariable String plantCategory, #PathVariable String plantName) {
// if plantCateogry is not found in database, continue with next controller - as there is no more, it should return 404
}
You can achieve this problem with a general controller method like this:
// General controller method
#GetMapping(path = "/{category [a-z-]+}/{name [a-z-]+}")
public void show(#PathVariable String category, #PathVariable String name) {
// look in database for the category
if(isAnimalCatagory) {
return showAnimal(category, name);
}
else if(isPlantCategory) }
return showPlant(category, name);
}
return "redirect:/404";
}
public void showAnimal(String animalCategory, String animalName) {
// for animal categories
}
public void showPlant(String plantCategory, String plantName) {
// for plant categories
}

Is it possible to drive the #Size "max" value from a properties file?

I'm using Spring 3.1.1.RELEASE. I have a model with the following attribute
import javax.validation.constraints.Size;
#Size(max=15)
private String name;
I validate the model in my controller my running
#RequestMapping(value = "/save", method = RequestMethod.POST)
public ModelAndView save(final Model model,
#Valid final MyForm myForm,
I would like to have the "15" value come from a properties file instead of hard-coded, but am unclear if that's possible or how its done. Any ideas?
This is not possible. The constant expression that you provide as a value for the max attribute is added at compile time. There is no way to change the value of an annotation at runtime. Setting it from a properties file you read is therefore not possible
What you can do instead is to create and register your own Validator for the your class that has that field. For example,
public class MyValidator implements Validator {
public void validate(Object target, Errors errors) {
MyObject obj = (MyObject) target;
int length = getProperties().get("max.size");
if (obj.name.length() > length) {
errors.rejectValue("name", "String length is bigger than " + length);
}
}
public boolean supports(Class<?> clazz) {
return clazz == MyOBject.class;
}
}
Take a look at Spring's validation framework.

How do you handle deserializing empty string into an Enum?

I am trying to submit a form from Ext JS 4 to a Spring 3 Controller using JSON. I am using Jackson 1.9.8 for the serialization/deserialization using Spring's built-in Jackson JSON support.
I have a status field that is initially null in the Domain object for a new record. When the form is submitted it generates the following json (scaled down to a few fields)
{"id":0,"name":"someName","status":""}
After submitted the following is seen in the server log
"nested exception is org.codehaus.jackson.map.JsonMappingException: Can not construct instance of com.blah.domain.StatusEnum from String value '': value not one of the declared Enum instance names"
So it appears that Jackson is expecting a valid Enum value or no value at all including an empty string. How do I fix this whether it is in Ext JS, Jackson or Spring?
I tried to create my own ObjectMapper such as
public class MyObjectMapper extends Object Mapper {
public MyObjectMapper() {
configure(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
}
}
and send this as a property to MappingJacksonMappingView but this didn't work. I also tried sending it in to MappingJacksonHttpMessageConverter but that didn't work. Side question: Which one should I be sending in my own ObjectMapper?
Suggestions?
The other thing you could do is create a specialized deserializer (extends org.codehaus.jackson.map.JsonDeserializer) for your particular enum, that has default values for things that don't match. What I've done is to create an abstract deserializer for enums that takes the class it deserializes, and it speeds this process along when I run into the issue.
public abstract class EnumDeserializer<T extends Enum<T>> extends JsonDeserializer<T> {
private Class<T> enumClass;
public EnumDeserializer(final Class<T> iEnumClass) {
super();
enumClass = iEnumClass;
}
#Override
public T deserialize(final JsonParser jp,
final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final String value = jp.getText();
for (final T enumValue : enumClass.getEnumConstants()) {
if (enumValue.name().equals(value)) {
return enumValue;
}
}
return null;
}
}
That's the generic class, basically just takes an enum class, iterates over the values of the enum and checks the next token to match any name. If they do it returns it otherwise return null;
Then If you have an enum MyEnum you'd make a subclass of EnumDeserializer like this:
public class MyEnumDeserializer extends EnumDeserializer<MyEnum> {
public MyEnumDeserializer() {
super(MyEnum.class);
}
}
Then wherever you declare MyEnum:
#JsonDeserialize(using = MyEnumDeserializer.class)
public enum MyEnum {
...
}
I'm not familiar with Spring, but just in case, it may be easier to handle that on the client side:
Ext.define('My.form.Field', {
extend: 'Ext.form.field.Text',
getSubmitValue: function() {
var me = this,
value;
value = me.getRawValue();
if ( value === '' ) {
return ...;
}
}
});
You can also disallow submitting empty fields by setting their allowBlank property to false.
Ended up adding defaults in the EXT JS Model so there is always a value. Was hoping that I didn't have to this but it's not that big of a deal.
I have the same issue. I am reading a JSON stream with some empty strings. I am not in control of the JSON stream, because it is from a foreign service. And I am always getting the same error message. I tried this here:
mapper.getDeserializationConfig().with(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
But without any effect. Looks like a Bug.

Resources