CustomBinder in Spring MVC for processing list of objects - spring-mvc

I have spring mvc form from which i am submitting values of a cusomized object A. The customized object A has one of its field as a list of another object B.
In the controller, I can see the size of list of object B but it is throwing error message of
Failed to convert property value of type 'java.lang.String' to required type 'java.util.List'
Now I have created a class CustomBinder which is following
public class CustomBinder extends PropertyEditorSupport {
private List<b> listOfb;
public CustomBinder(List<b> listOfb){
this.listOfb = listOfb;
}
#Override
public void setAsText(String text) throws IllegalArgumentException {
// TODO Auto-generated method stub
setValue(listOfb);
}
#Override
public String getAsText() {
// TODO Auto-generated method stub
return super.getAsText();
}
}
and in the controller, I have registered the custombinder
#InitBinder
protected void initBinder(Object target, WebDataBinder binder)
{
binder.setValidator(this.productUniqueValidator);
A a = (A) target;
List<B> b= a.getLstOfB;
binder.registerCustomEditor(List.class,"bs" ,new CustomBinder(b));
}
I am still getting a error message...Not sure what i am doing wrong !!

Related

Spring #ControllerAdvice messageSource doesn't work with Hibernate message parameters

I'm trying to add some custom bean validation in a Spring Boot REST controller, extending the ResponseEntityExceptionHandler class with a #ControllerAdvice annotation and overriding the #handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, HttpStatus status, WebRequest request) method. In this method I'm trying to convert the given FieldError via the messageSource into a localized message. Although I'm receiving a NumberFormatException when trying to using message parameters which are be possible via the Hibernate validator.
I'm using the following dependencies:
org.hibernate.validator:hibernate-validator (6.0.11.Final)
org.springframework:spring-web (5.0.8.RELEASE)
org.springframework:spring-webmvc (5.0.8.RELEASE)
All included via org.springframework.boot:spring-boot-starter-web (2.0.4.RELEASE).
Consider using the following REST controller:
#RestController
public class FooController {
#PostMapping(value = "/foo")
public void submitFooRequest(#Validated #RequestBody FooRequest fooRequest) {
// ....
}
}
The FooRequest bean has a custom bean validation annotation & constraint validator:
The bean FooRequest:
#Getter
#Setter
#ValidBarRequest
public class FooRequest {
private String fieldFoo;
private BarRequest barRequest;
}
The bean BarRequest:
#Getter
#Setter
public class BarRequest {
private String fieldBar;
}
The validation annotation:
#Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = BarRequestValidator.class)
#Documented
public #interface ValidBarRequest {
String message() default "{org.example.validation.constraints.ValidBarRequest.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String fieldFoo() default "fieldFoo";
String barRequestFieldBar() default "barRequest.fieldBar";
}
The validation constraint validator:
#Log4j2
public class BarRequestValidator implements ConstraintValidator<ValidBarRequest, Object> {
// ....
#Override
public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
if (/* some condition */) {
HibernateConstraintValidatorContext hibernateValidatorContext = constraintValidatorContext.unwrap(HibernateConstraintValidatorContext.class);
hibernateValidatorContext.disableDefaultConstraintViolation();
hibernateValidatorContext.addMessageParameter("fieldFoo", "some value...").buildConstraintViolationWithTemplate("{org.example.validation.constraints.ValidBarRequest.message}")
.addPropertyNode("barRequest.fieldBar").addConstraintViolation();
return false;
}
return true;
}
}
However via a #ControllerAdvice annotated bean and using a Spring messageSource a NumberFormatException is thrown on the following message (in messages.properties):
ValidBarRequest.fooRequest.barRequest.fieldBar=must be lower or equal than {fieldFoo}
The #ControllerAdvice bean:
#ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
#Autowired private MessageSource messageSource;
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, HttpStatus status, WebRequest request) {
List<ErrorDetails> errorDetails = new ArrayList<>();
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
errorDetails.add(new ErrorDetails(fieldError.getField(), messageSource.getMessage(fieldError, Locale.getDefault())));
}
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
#Getter
#AllArgsConstructor
class ErrorDetails {
private String field;
private String message;
}
}
This causes the following exception: Caused by: java.lang.NumberFormatException: For input string: "fieldFoo"
What am I doing wrong? Also I included the following bean in my #SpringBootApplication:
#Bean
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean localValidatorFactory = new LocalValidatorFactoryBean();
localValidatorFactory.setValidationMessageSource(messageSource);
return localValidatorFactory;
}
The populate the variables in the message source MessageFormat.format is and there you must use numbers in curly braces.
ValidBarRequest.fooRequest.barRequest.fieldBar=must be lower or equal than {0}

Throw custom exception in spring boot with an Autowired variable

A custom exception that requires the use of an Autowired variable
// #Component // should be a component to use autowire variable
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public class MyException extends Exception {
// This should be commented in
// #Autowired
// public RequestModel reqModel;
// public MyException(ExceptionType type) {
public MyException(ExceptionType type, RequestModel reqModel) {
super();
switch type {
// ...
}
}
}
And then in the rest controllers and interceptors we throw the error
public class MyInterceptor extends HandlerInterceptorAdapter {
// These should be commented out
#Autowired
public RequestModel reqModel;
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// This should be thrown without adding the reqModel as a param
// Unsure how to throw this without using "new"
throw new MyException.MyException(PARAM_MISSING, reqModel);
}
}
I understand that instantiating a class using new will null the Autowired variables. When I tried dropping the new and converting the MyException class to a #Component, I see issues with calling the MyException class constructor.
What is the best way to implement a custom exception in springboot using an autowired variable?

Return 404 for every null response

I'd like to return 404 when the response object is null for every response automatically in spring boot.
I need suggestions.
I don't want to check object in controller that it is null or not.
You need more than one Spring module to accomplish this. The basic steps are:
Declare an exception class that can be used to throw an exception when a repository method does not return an expected value.
Add a #ControllerAdvice that catches the custom exception and translates it into an HTTP 404 status code.
Add an AOP advice that intercepts return values of repository methods and raises the custom exception when it finds the values not matching expectations.
Step 1: Exception class
public class ResourceNotFoundException extends RuntimeException {}
Step 2: Controller advice
#ControllerAdvice
public class ResourceNotFoundExceptionHandler
{
#ExceptionHandler(ResourceNotFoundException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
public void handleResourceNotFound() {}
}
Step 3: AspectJ advice
#Aspect
#Component
public class InvalidRepositoryReturnValueAspect
{
#AfterReturning(pointcut = "execution(* org.example.data.*Repository+.findOne(..))", returning = "result")
public void intercept(final Object result)
{
if (result == null)
{
throw new ResourceNotFoundException();
}
}
}
A sample application is available on Github to demonstrate all of this in action. Use a REST client like Postman for Google Chrome to add some records. Then, attempting to fetch an existing record by its identifier will return the record correctly but attempting to fetch one by a non-existent identifier will return 404.
Simplest way to do this in Spring is write your own exception class like below
#ResponseStatus(value = HttpStatus.NOT_FOUND)
class ResourceNotFoundException extends RuntimeException{
}
Then just throw the ResourceNotFoundException from anywhere.
if (something == null) throw new ResourceNotFoundException();
For more -> Read
Similar to #manish's answer (https://stackoverflow.com/a/43891952/986160) but without the AspectJ pointcut and using another #ControllerAdvice instead:
Step 1: NotFoundException class:
public class NotFoundException extends RuntimeException {
public NotFoundException(String msg) {
super(msg);
}
public NotFoundException() {}
}
Step 2: Check if body returned in endpoint is null and throw NotFoundException:
#ControllerAdvice
public class NotFoundAdvice implements ResponseBodyAdvice {
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
#SuppressWarnings("unchecked")
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body == null) {
throw new NotFoundException("resource not found");
}
return body;
}
}
Step 3: handle NotFoundException and make the response have a status of 404
#ControllerAdvice
public class GlobalExceptionAdvice {
#Data
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
}
#ExceptionHandler(NotFoundException.class)
public final ResponseEntity<ErrorDetails> notFoundHandler(Exception ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
}
Alternative to Step 3:
You can just annotate your NotFoundException with #ResponseStatus and override fillInStackTrace() (from https://stackoverflow.com/a/31263942/986160) so that it has similar effect to GlobalExceptionAdvice and doesn't show stacktrace like this:
#ResponseStatus(value = HttpStatus.NOT_FOUND,reason = "resource not found")
public class NotFoundException extends RuntimeException {
public NotFoundException(String msg) {
super(msg);
}
public NotFoundException() {}
#Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
There is no need to throw exceptions, now ResponseBodyAdvice does the trick:
#ControllerAdvice
public class NullTo404 implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body == null) {
response.setStatusCode(HttpStatus.NOT_FOUND);
}
return body;
}
}
Similarly, you can implement ResponseBodyAdvice<Optional<?>>, and check for Optional.isEmpty() before setting the response status. It has the added benefit of working nicely with CrudRepository. Most controller methods eventually ends like this:
public Optional<Product> getProductBySku(#PathVariable String sku) {
// logic goes here...
return productRepository.findBySku(sku);
}

Spring MVC - PropertyEditor not called during ModelAttribute type conversion

Using Spring 3.2.3, I'm trying to implement a simple CRUD controller that handles REST-ful URLs. It relies on a PropertyEditor to convert a path variable to a BusinessService entity by loading it from an application service. Code is as follows:
#Controller
public class BusinessServiceController {
#Autowired
private BusinessServiceService businessSvcService;
public BusinessServiceController() {
}
#InitBinder
public void initBinder(final WebDataBinder binder) {
binder.registerCustomEditor(BusinessService.class, new BusinessServicePropertyEditor(businessSvcService));
}
#RequestMapping(value = "/ui/account/business-services/{businessSvc}", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ModelAndView update(#ModelAttribute("businessSvc") #Valid final BusinessService businessSvc, final BindingResult result,
final RedirectAttributes redirectAttribs) throws UnknownBusinessServiceException {
ModelAndView mav;
if (result.hasErrors()) {
mav = new ModelAndView("/business-service/edit");
}
else {
businessSvcService.updateBusinessService(XSecurity.principal().getId(), businessSvc);
mav = new ModelAndView("redirect:/ui/account/business-services");
redirectAttribs.addFlashAttribute("message", Message.info("businessService.updated", businessSvc.getTitle()));
}
return mav;
}
}
public class BusinessServicePropertyEditor extends PropertyEditorSupport {
private final BusinessServiceService businessSvcService;
public BusinessServicePropertyEditor(final BusinessServiceService businessSvcService) {
this.businessSvcService = businessSvcService;
}
#Override
public String getAsText() {
final BusinessService svc = (BusinessService) getValue();
return Long.toString(svc.getId());
}
#Override
public void setAsText(final String text) {
final BusinessService svc = businessSvcService.getBusinessService(Long.parseLong(text));
setValue(svc);
}
}
According to SPR-7608, starting from Spring 3.2, #ModelAttribute method argument resolution checks if a path variable by the same name exists (it does here), in which case it tries to convert that path variable's value to the target parameter type through registered Converters and PropertyEditors. This is not what I'm experiencing. When I inspect what ServletModelAttributeMethodProcessor does, it clearly uses the request DataBinder's ConversionService to perform type conversion, which does not consider registered PropertyEditors, and hence BusinessServicePropertyEditor#setAsText is never called.
Is this a configuration problem or an actual bug?
Thanks for your help!
Spring's ConversionService and Converters are replacement for standard Java Beans PropertyEditors.
You need to implement Converter instead of PropertyEditor if this feature is based purely on conversion service.
To register your custom converters in WebDataBinder you might use ConfigurableWebBindingInitializer or #InitBinder method.

Abstract Factory Method pattern not working with Spring MVC

I have problem with Abstract Factory pattern implementation on Spring. I am using Spring 3 MVC and Hibernate 3, which works fine if I don't use Abstract Factory Method pattern.
I am not sure what I have to add to the Controller to have access the Factory Class (CategoryFactory).
Is there anything missing in the Controller or bean initiating ?
class SectionsController extends MultiActionController {
/* Do I have to initiate the CategoryFactory here? */
public ModelAndView secList() throws Exception {
CategoryFactory.CategoryType type = CategoryFactory.CategoryType.valueOf("view");
modelMap.addAttribute("sectionList", CategoryFactory.findCategory(type).list(id));
return new ModelAndView("Form", modelMap);
}
}
Abstract Factory
public abstract class Category {
public abstract List list(int departId);
}
public class CategoryFactory {
public enum CategoryType { firstclass, secondClass, ... }
public static Category findCategory(CategoryType categoryType) {
// Create type specific Category implementation
}
}
It shoud be:
class SectionsController extends MultiActionController {
private HibernateTemplate hibernateTemplate;
public void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
}
public ModelAndView secList() throws Exception {
CategoryFactory.CategoryType type=CategoryFactory.CategoryType.valueOf("view");
modelMap.addAttribute("sectionList",CategoryFactory.findCategory(type).list(hibernateTemplate,id);
return new ModelAndView("Form", modelMap);
}
}

Resources