16

I am working on a Spring web app and i have an entity that has an Integer property which the user can fill in when creating a new entity using a JSP form. The controller method called by this form is below :

@RequestMapping(value = {"/newNursingUnit"}, method = RequestMethod.POST)
public String saveNursingUnit(@Valid NursingUnit nursingUnit, BindingResult result, ModelMap model) 
{
    boolean hasCustomErrors = validate(result, nursingUnit);
    if ((hasCustomErrors) || (result.hasErrors()))
    {
        List<Facility> facilities = facilityService.findAll();
        model.addAttribute("facilities", facilities);

        setPermissions(model);

        return "nursingUnitDataAccess";
    }

    nursingUnitService.save(nursingUnit);
    session.setAttribute("successMessage", "Successfully added nursing unit \"" + nursingUnit.getName() + "\"!");
    return "redirect:/nursingUnits/list";
}

The validate method simply checks if the name already exists in the DB so I did not include it. My issue is that, when I purposely enter text in the field, I would like to have a nice message such as "The auto-discharge time must be a number!". Instead, Spring returns this absolutely horrible error :

Failed to convert property value of type [java.lang.String] to required type [java.lang.Integer] for property autoDCTime; nested exception is java.lang.NumberFormatException: For input string: "sdf"

I fully understand why this is happening but i cannot for the life of me figure out how to, programmatically, replace Spring's default number format exception error message with my own. I am aware of message sources which can be used for this type of thing but I really want to achieve this directly in the code.

EDIT

As suggested, i built this method in my controller but i'm still getting Spring's "failed to convert property value..." message :

@ExceptionHandler({NumberFormatException.class})
private String numberError()
{
   return "The auto-discharge time must be a number!";
}

OTHER EDIT

Here is the code for my entity class :

@Entity
@Table(name="tblNursingUnit")
public class NursingUnit implements Serializable 
{
private Integer id;
private String name;
private Integer autoDCTime;
private Facility facility;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getId() 
{
    return id;
}

public void setId(Integer id) 
{
    this.id = id;
}

@Size(min = 1, max = 15, message = "Name must be between 1 and 15 characters long")
@Column(nullable = false, unique = true, length = 15)
public String getName() 
{
    return name;
}

public void setName(String name) 
{
    this.name = name;
}

@NotNull(message = "The auto-discharge time is required!")
@Column(nullable = false)
public Integer getAutoDCTime() 
{
    return autoDCTime;
}

public void setAutoDCTime(Integer autoDCTime) 
{
    this.autoDCTime = autoDCTime;
}

@ManyToOne (fetch=FetchType.EAGER)
@NotNull(message = "The facility is required")
@JoinColumn(name = "id_facility", nullable = false)
public Facility getFacility()
{
    return facility;
}

public void setFacility(Facility facility)
{
    this.facility = facility;
}

@Override
public boolean equals(Object obj) 
{
    if (obj instanceof NursingUnit)
    {
        NursingUnit nursingUnit = (NursingUnit)obj;
        if (Objects.equals(id, nursingUnit.getId()))
        {
            return true;
        }
    }
    return false;
}

@Override
public int hashCode() 
{
    int hash = 3;
    hash = 29 * hash + Objects.hashCode(this.id);
    hash = 29 * hash + Objects.hashCode(this.name);
    hash = 29 * hash + Objects.hashCode(this.autoDCTime);
    hash = 29 * hash + Objects.hashCode(this.facility);
    return hash;
}

@Override
public String toString()
{
    return name + " (" + facility.getCode() + ")";
}
}

YET ANOTHER EDIT

I am able to make this work using a message.properties file on the classpath containing this :

typeMismatch.java.lang.Integer={0} must be a number!

And the following bean declaration in a config file :

@Bean
public ResourceBundleMessageSource messageSource() 
{
    ResourceBundleMessageSource resource = new ResourceBundleMessageSource();
    resource.setBasename("message");
    return resource;
}

This gives me the correct error message instead of the Spring generic TypeMismatchException / NumberFormatException which i can live with but still, I want to do everything programmatically wherever possible and I'm looking for an alternative.

Thank you for your help!

Martin
  • 1,977
  • 5
  • 30
  • 67
  • Absolutely. I'm trying to make my app as fool-proof as possible so i'm trying to make the error returned from the server a little nicer when someone erroneously (or on purpose in my case) enters text in a field "mapped" to an Integer value. – Martin Feb 06 '18 at 20:25
  • Ok, I guessed so. The solution I suggested is also described in the official documentation, it should work. I will update the answer with a link and perhaps it can help you understand why it doesn't work. – NiVeR Feb 06 '18 at 20:26
  • Thank you, i will read the article and see if I can't spot something i'm doing wrong. – Martin Feb 06 '18 at 20:39
  • @Martin Hi Martin, what version of spring are you using? – LoolKovsky Jun 20 '18 at 10:34
  • 5.0.4 for now, although i want to upgrade to the latest release in the very near future. – Martin Jun 20 '18 at 11:09
  • in fact it is now 5.0.7 which is the latest – Martin Jun 20 '18 at 13:18

5 Answers5

3

You may be able to override that messaging by providing an implementation of the Spring DefaultBindingErrorProcessor similar to what is done here: Custom Binding Error Message with Collections of Beans in Spring MVC

Joe W
  • 2,773
  • 15
  • 35
  • Thank you for your answer! Suppose i have this in my message.properties file and everything works fine currently : typeMismatch.nursingUnit.autoDCTime=The auto-discharge time must be a number! How would i replicate returning this error message? Not sure i understand since the method returns an array of Strings? – Martin Jun 18 '18 at 19:15
  • In your case all you need to return is a String value that holds the error you want. It is an array in the event a field fails multiple checks and you need multiple messages. You may need to add an annotation to the field so that you can get an annotation validation failure rather than a conversion one. I'm not entirely sure on a conversion failure. I know for sure it gets called on annotation validation failures. The format of the message there looks the same as the annotation ones. In your case you probably can just check the field and if it matches AutoDCTime return your error message. – Joe W Jun 18 '18 at 19:34
  • I built the class you suggested, annotated it with @Component and made sure it was in a scanned package. I added a breakpoint in the method you mentioned and it is never reached when i try to validate a non-number in the autoDCTime field on the form. – Martin Jun 18 '18 at 19:57
  • In doing more reading it looks like the DefaultmessageCodesResolver is only for the validation annotations that occur after type checking.I think this will require an override of DefaultBindingErrorProcessor. Updated answer with that thought thought message.properties as you have it might be best in the end – Joe W Jun 18 '18 at 20:17
2

You can annotate a method with:

@ExceptionHandler({NumberFormatException.class})
public String handleError(){
   //example
   return "Uncorrectly formatted number!";
}

and implement whatever you want to do in case the exception of that type is thrown. The given code will handle exceptions happened in the current controller. For further reference consult this link.

To make global error handling you can use @ControllerAdvice in the following way:

@ControllerAdvice
public class ServiceExceptionHandler extends ResponseEntityExceptionHandler {

   @ExceptionHandler({NumberFormatException.class})
    public String handleError(){
       //example
       return "Uncorrectly formatted number!";
    }
} 
NiVeR
  • 9,644
  • 4
  • 30
  • 35
  • I do recall trying something like that but I couldn't figure out what to actually put in there. When i generate a custom error I do something like FieldError error = new FieldError("nursingUnit", "name", nursingUnit.getName(), false, null, null, nursingUnit.getName() + " already exists!"); What code would i put in that method to tell Spring to replace its ugly error by my version? – Martin Feb 06 '18 at 19:58
  • I added this to my controller and i'm still getting Spring's failed to convert property value message : @ExceptionHandler({NumberFormatException.class}) private String numeberError() { return "The auto-discharge time must be a number!"; } – Martin Feb 06 '18 at 20:07
  • Where is the `autoDCTime` variable? I don't see it in the the api you wrote. – NiVeR Feb 06 '18 at 20:10
  • the autoDCTime property is attached to the NursingUnit object being validated by my saveNursingUnit method above. Is that method supposed to go in the controller or in the entity class itself? – Martin Feb 06 '18 at 20:12
  • Please update the question by including the code of the NursingUnit. – NiVeR Feb 06 '18 at 20:13
  • @Martin Do you see any change if you modify your new exception handling method to be public rather than private? – takendarkk Feb 06 '18 at 20:14
  • changing the method from private to public sadly has no effect – Martin Feb 06 '18 at 20:17
  • 1
    The `@ExceptionHandler` only applies to errors occurring within your controller or anything it calls. The exception is thrown because it cannot map the POST to your controller input, which is happening outside your code. – Jean Marois Jun 19 '18 at 06:09
  • @JeanMarois I tried the example , it does work. The exception happens indeed in the controller, where values are mapped to parameters – NiVeR Jun 22 '18 at 08:03
  • @NiVeR Good to know! I must have been thinking of `@PathParams` because if the HttpRequest parameters cannot be mapped to a `@RequestMapping` then Spring would not know to invoke the `@Controller`'s specific `@ExceptionHandler`. `@ControllerAdvice` is a different story, and I agree is the preferred approach. – Jean Marois Jun 23 '18 at 17:43
0

@Martin, I asked you about the version because @ControllerAdvice is available starting with version 3.2.

I would recommend you to use @ControllerAdvice, which is an annotation that allows you to write code that is sharable between controllers(annotated with @Controller and @RestController), but it can also be applied only to controllers in specific packages or concrete classes.

ControllerAdvice is intended to be used with @ExceptionHandler, @InitBinder, or @ModelAttribute.

You set the target classes like this @ControllerAdvice(assignableTypes = {YourController.class, ...}).

@ControllerAdvice(assignableTypes = {YourController.class, YourOtherController.class})
public class YourExceptionHandler{
    //Example with default message
    @ExceptionHandler({NumberFormatException.class})
    private String numberError(){
        return "The auto-discharge time must be a number!";
    }

    //Example with exception handling
    @ExceptionHandler({WhateverException.class})
    private String whateverError(WhateverException exception){
        //do stuff with the exception
        return "Whatever exception message!";
    }

    @ExceptionHandler({ OtherException.class })
    protected String otherException(RuntimeException e, WebRequest request) {
        //do stuff with the exception and the webRequest
        return "Other exception message!";
    }
} 

What you need to keep in mind is that if you do not set the target and you define multiple exception handlers for the same exceptions in different @ControllerAdvice classes, Spring will apply the first handler that it finds. If multiple exception handlers are present in the same @ControllerAdvice class, an error will be thrown.

LoolKovsky
  • 1,018
  • 1
  • 12
  • 29
  • @Martin, sorry for the late post, I prepared it yesterday but I was not able to post it until now. I also added small examples of how you can extend the use of the "ExceptionHandler" annotation for different situations in which you might need to use the request or the exception itself. – LoolKovsky Jun 22 '18 at 13:08
  • It is the same (or very similar) answer as mine. – NiVeR Jun 22 '18 at 16:19
0

Solution 1: StaticMessageSource as Spring bean

This gives me the correct error message instead of the Spring generic TypeMismatchException / NumberFormatException which i can live with but still, I want to do everything programmatically wherever possible and I'm looking for an alternative.

Your example uses ResourceBundleMessageSource which uses resource bundles (such as property files). If you want to use everything programmatically, then you could use a StaticMessageSource instead. Which you can then set as a Spring bean named messageSource. For example:

@Configuration
public class TestConfig {
    @Bean
    public MessageSource messageSource() {
        StaticMessageSource messageSource = new StaticMessageSource();
        messageSource.addMessage("typeMismatch.java.lang.Integer", Locale.getDefault(), "{0} must be a number!");
        return messageSource;
    }
}

This is the simplest solution to get a user friendly message.

(Make sure the name is messageSource.)

Solution 2: custom BindingErrorProcessor for initBinder

This solution is lower level and less easy than solution 1, but may give you more control:

public class CustomBindingErrorProcessor extends DefaultBindingErrorProcessor {
    public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) {
        Throwable cause = ex.getCause();
        if (cause instanceof NumberFormatException) {
            String field = ex.getPropertyName();
            Object rejectedValue = ex.getValue();
            String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field);
            Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), field);

            boolean useMyOwnErrorMessage = true; // just so that you can easily see to default behavior one line below
            String message = useMyOwnErrorMessage ? field + " must be a number!" : ex.getLocalizedMessage();
            FieldError error = new FieldError(bindingResult.getObjectName(), field, rejectedValue, true, codes, arguments, message);
            error.wrap(ex);
            bindingResult.addError(error);
        } else {
            super.processPropertyAccessException(ex, bindingResult);
        }
    }
}

@ControllerAdvice
public class MyControllerAdvice {
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        BindingErrorProcessor customBindingErrorProcessor = new CustomBindingErrorProcessor();
        binder.setBindingErrorProcessor(customBindingErrorProcessor);
    }
}

It basically intercepts the call to DefaultBindingErrorProcessor.processPropertyAccessException and adds a custom FieldError message when binding failed with a NumberFormatException.

Example code without Spring Web/MVC

In case you want to try it without Spring Web/MVC, but just plain Spring, then you could use this example code.

public class MyApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
        Validator validator = context.getBean(LocalValidatorFactoryBean.class);

        // Empty person bean to be populated
        Person2 person = new Person2(null, null);
        // Data to be populated
        MutablePropertyValues propertyValues = new MutablePropertyValues(List.of(
                new PropertyValue("name", "John"),
                // Bad value
                new PropertyValue("age", "anInvalidInteger")
        ));

        DataBinder dataBinder = new DataBinder(person);
        dataBinder.setValidator(validator);
        dataBinder.setBindingErrorProcessor(new CustomBindingErrorProcessor());

        // Bind and validate
        dataBinder.bind(propertyValues);
        dataBinder.validate();

        // Get and print results
        BindingResult bindingResult = dataBinder.getBindingResult();
        bindingResult.getAllErrors().forEach(error -> 
                System.out.println(error.getDefaultMessage())
        );
        
        // Output:
        // "age must be a number!"
    }
}

@Configuration
class MyConfig {
    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}

class Person2 {
    @NotEmpty
    private String name;

    @NotNull @Range(min = 20, max = 50)
    private Integer age;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    public Person2(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

Devabc
  • 4,782
  • 5
  • 27
  • 40
-2

Handle NumberFormatException.

try {
 boolean hasCustomErrors = validate(result, nursingUnit);
}catch (NumberFormatException nEx){
 // do whatever you want
 // for example : throw custom Exception with the custom message.
}
Sundararaj Govindasamy
  • 8,180
  • 5
  • 44
  • 77
  • The validate method is my own and it does not throw any exceptions. The ugly error is generated by Spring itself and i'm looking to somehow replace it. – Martin Feb 06 '18 at 19:52
  • Do you believe NumberFormatException is thrown by Spring? Nope. – Sundararaj Govindasamy Feb 06 '18 at 20:13
  • It most definitely is not thrown by the code i wrote and I would argue that it is indeed thrown by Spring when trying to convert the erroneously entered String data entered by the user to an Integer type which my entity class requires... – Martin Feb 06 '18 at 20:22
  • 3
    Too bad your answer demonstrates a complete and utter lack of understanding of the question i'm asking, you would have been credible. My validate method does not throw a NumberFormatException, it does absolutely nothing else than add a FieldError to the model for the name field if it's already present in the database. – Martin Feb 06 '18 at 20:27