Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 4535

Guide to Spring Data REST Validators

$
0
0

The Master Class of "Learn Spring Security" is out:

>> CHECK OUT THE COURSE

1. Overview

This article covers a basic introduction to Spring Data REST Validators. If you need to first go over the basics of Spring Data REST, definitely visit this article to brush up on the basics.

Simply put, with Spring Data REST, we can simply add a new entry into the database through the REST API, but we of course also need to make sure the data is valid before actually persisting it.

This article continues on an existing article and we will reuse the existing project we set up there.

2. Using Validators

Starting with Spring 3, the framework features the Validator interface – which can be used to validate objects.

2.1. Motivation

In the previous article, we defined our entity having two properties – name and email.

And so, to create a new resource, we simply need to run:

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "name" : "Test", "email" : "test@test.com" }' 
  http://localhost:8080/users

This POST request will save the provided JSON object into our database, and the operation will return:

{
  "name" : "Test",
  "email" : "test@test.com",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/1"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/1"
    }
  }
}

A positive outcome was expected since we provided valid data. But, what will happen if we remove the property name, or just set the value to an empty String?

To test the first scenario, we will run modified command from before where we will set empty string as a value for property name:

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "name" : "", "email" : "Baggins" }' http://localhost:8080/users

With that command we’ll get the following response:

{
  "name" : "",
  "email" : "Baggins",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/1"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/1"
    }
  }
}

For the second scenario, we will remove property name from request:

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "email" : "Baggins" }' http://localhost:8080/users

For that command we will get this response:

{
  "name" : null,
  "email" : "Baggins",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/2"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/2"
    }
  }
}

As we can see, both requests were OK and we can confirm that with 201 status code and API link to our object.

This behavior is not acceptable since we want to avoid inserting partial data into a database.

2.2. Spring Data REST Events

During every call on Spring Data REST API, Spring Data REST exporter generates various events which are listed here:

  • BeforeCreateEvent
  • AfterCreateEvent
  • BeforeSaveEvent
  • AfterSaveEvent
  • BeforeLinkSaveEvent
  • AfterLinkSaveEvent
  • BeforeDeleteEvent
  • AfterDeleteEvent

Since all events are handled in a similar way, we will only show how to handle beforeCreateEvent which is generated before a new object is saved into the database.

2.3. Defining a Validator

To create our own validator, we need to implement the org.springframework.validation.Validator interface with the supports and validate methods.

Supports checks if the validator supports provided requests, while validate method validates provided data in requests.

Let’s define a WebsiteUserValidator class:

public class WebsiteUserValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return WebsiteUser.class.equals(clazz);
    }

    @Override
    public void validate(Object obj, Errors errors) {
        WebsiteUser user = (WebsiteUser) obj;
        if (checkInputString(user.getName())) {
            errors.rejectValue("name", "name.empty");
        }
   
        if (checkInputString(user.getEmail())) {
            errors.rejectValue("email", "email.empty");
        }
    }

    private boolean checkInputString(String input) {
        return (input == null || input.trim().length() == 0);
    }
}

Errors object is a special class designed to contain all errors provided in validate method. Later in this article, we’ll show how you can use provided messages contained in Errors object.
To add new error message, we have to call errors.rejectValue(nameOfField, errorMessage).

After we’ve defined the validator, we need to map it to a specific event which is generated after the request is accepted.

For example, in our case, beforeCreateEvent is generated because we want to insert a new object into our database. But since we want to validate object in a request, we need to define our validator first.

This can be done in three ways:

  • Add Component annotation with name “beforeCreateWebsiteUserValidator“. Spring Boot will recognize prefix beforeCreate which determines the event we want to catch, and it will also recognize WebsiteUser class from Component name.
    @Component("beforeCreateWebsiteUserValidator")
    public class WebsiteUserValidator implements Validator {
        ...
    }
  • Create Bean in Application Context with @Bean annotation:
    @Bean
    public WebsiteUserValidator beforeCreateWebsiteUserValidator() {
        return new WebsiteUserValidator();
    }
  • Manual registration:
    @SpringBootApplication
    public class SpringDataRestApplication 
      extends RepositoryRestMvcConfiguration {
        public static void main(String[] args) {
            SpringApplication.run(SpringDataRestApplication.class, args);
        }
        
        @Override
        protected void configureValidatingRepositoryEventListener(
          ValidatingRepositoryEventListener v) {
            v.addValidator("beforeCreate", new WebsiteUserValidator());
        }
    }
    • For this case, you don’t need any annotations on WebsiteUserValidator class.

2.4.  Event Discovery Bug

At the moment, a bug exists in Spring Data REST – which affects events discovery.

If we call POST request which generates the beforeCreate event, our application will not call validator because the event will not be discovered, due to this bug.

A simple workaround for this problem is to insert all events into Spring Data REST ValidatingRepositoryEventListener class:

@Configuration
public class ValidatorEventRegister implements InitializingBean {

    @Autowired
    ValidatingRepositoryEventListener validatingRepositoryEventListener;

    @Autowired
    private Map<String, Validator> validators;

    @Override
    public void afterPropertiesSet() throws Exception {
        List<String> events = Arrays.asList("beforeCreate");
        for (Map.Entry<String, Validator> entry : validators.entrySet()) {
            events.stream()
              .filter(p -> entry.getKey().startsWith(p))
              .findFirst()
              .ifPresent(
                p -> validatingRepositoryEventListener
               .addValidator(p, entry.getValue()));
        }
    }
}

3. Testing

In Section 2.1. we showed that, without a validator, we can add objects without name property into our database which is not desired behavior because we don’t check data integrity.

If we want to add the same object without name property but with provided validator, we will get this error:

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "email" : "test@test.com" }' http://localhost:8080/users
{  
   "timestamp":1472510818701,
   "status":406,
   "error":"Not Acceptable",
   "exception":"org.springframework.data.rest.core.
    RepositoryConstraintViolationException",
   "message":"Validation failed",
   "path":"/users"
}

As we can see, missing data from request was detected and an object was not saved into the database. Our request was returned with 500 HTTP code and message for an internal error.

The error message doesn’t say anything about the problem in our request. If we want to make it more informational, we will have to modify response object.

In the Exception Handling in Spring article, we showed how to handle exceptions generated by framework, so that’s definitely a good read at this point.

Since our application generates a RepositoryConstraintViolationException exception we will create a handler for this particular exception which will modify response message.

This is ours RestResponseEntityExceptionHandler class:

@ControllerAdvice
public class RestResponseEntityExceptionHandler extends
  ResponseEntityExceptionHandler {

    @ExceptionHandler({ RepositoryConstraintViolationException.class })
    public ResponseEntity<Object> handleAccessDeniedException(
      Exception ex, WebRequest request) {
          RepositoryConstraintViolationException nevEx = 
            (RepositoryConstraintViolationException) ex;

          String errors = nevEx.getErrors().getAllErrors().stream()
            .map(p -> p.toString()).collect(Collectors.joining("\n"));
          
          return new ResponseEntity<Object>(errors, new HttpHeaders(),
            HttpStatus.PARTIAL_CONTENT);
    }
}

With this custom handler, our return object will have information about all detected errors.

4. Conclusion

In this article, we showed that validators are essential for every Spring Data REST API which provides an extra layer of security for data insertion.

We also illustrated how simple is to create new validator with annotations.

As always, the code for this application can be found in the GitHub project.

The Master Class of "Learn Spring Security" is out:

>> CHECK OUT THE COURSE


Viewing all articles
Browse latest Browse all 4535

Trending Articles