[DATAREST-524] Validator auto discovery not working for Spring Data Rest Created: 22/Apr/15  Updated: 01/May/19

Status: Open
Project: Spring Data REST
Component/s: None
Affects Version/s: None
Fix Version/s: None

Type: Bug Priority: Minor
Reporter: Daniel Moses Assignee: Oliver Drotbohm
Resolution: Unresolved Votes: 30
Labels: spring-boot, spring-data-rest, validation
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Last updater: Rafael Renan Pacheco

 Description   

See documentation
http://docs.spring.io/spring-data/rest/docs/2.2.2.RELEASE/reference/html/#validation-chapter

Discovery should happen with Validator Prefix. Add a Validator bean to the context and notice that it does not get auto-detected. Manual wiring still works. Here is an example validator that will not work if included in the example Spring boot project:

    @Component("beforeCreatePersonValidator")
    public class BeforeCreatePersonValidator implements Validator {
        @Override
        public boolean supports(Class<?> clazz) {
            return Person.class.equals(clazz);
        }
        
        @Override
        public void validate(Object target, Errors errors) {
            errors.reject("TESTING");
        }
    }

See problem as reported:
http://stackoverflow.com/questions/24318405/spring-data-rest-validator



 Comments   
Comment by Fabian Trampusch [ 13/Jul/15 ]

Are there any news on this one?

Comment by Andreas Kluth [ 13/Jul/15 ]

You could add this configuration to add the expected behavior.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
import org.springframework.validation.Validator;

@Configuration
public class ValidatorRegistrar implements InitializingBean {

    private static final List<String> EVENTS;
    static {
        List<String> events = new ArrayList<String>();
        events.add("beforeCreate");
        events.add("afterCreate");
        events.add("beforeSave");
        events.add("afterSave");
        events.add("beforeLinkSave");
        events.add("afterLinkSave");
        events.add("beforeDelete");
        events.add("afterDelete");
        EVENTS = Collections.unmodifiableList(events);
    }

    @Autowired
    ListableBeanFactory beanFactory;

    @Autowired
    ValidatingRepositoryEventListener validatingRepositoryEventListener;

    @Override
    public void afterPropertiesSet() throws Exception {
        Map<String, Validator> validators = beanFactory.getBeansOfType(Validator.class);
        for (Map.Entry<String, Validator> entry : validators.entrySet()) {
            EVENTS.stream().filter(p -> entry.getKey().startsWith(p)).findFirst()
                    .ifPresent(p -> validatingRepositoryEventListener.addValidator(p, entry.getValue()));
        }
    }
}
Comment by Fabian Trampusch [ 18/Jul/15 ]

Thanks a lot, Andreas! I will try that approach.
Anyway, we should fix the docs or the behaviour.
I am not yet that familiar with the codebase. Any idea, if and where something is implemented regarding this?

Comment by Sebastian Bathke [ 03/Apr/16 ]

Thanks Andreas for providing the configuration! Had the same problem that validation beans with this prefix didn't got catched up as the documentation suggests.

However I encountered a problem that in some occasions (multiple repositories for the same entity) some tests failed to startup the context correctly with

No qualifying bean of type [org.springframework.data.rest.core.event.ValidatingRepositoryEventListener]

I could fix that by migrating your solution to RepositoryRestConfigurerAdapter:

@Configuration
public class ValidatorRegistrar extends RepositoryRestConfigurerAdapter {

    private static final List<String> EVENTS;

    static {
        List<String> events = new ArrayList<String>();
        events.add("beforeCreate");
        events.add("afterCreate");
        events.add("beforeSave");
        events.add("afterSave");
        events.add("beforeLinkSave");
        events.add("afterLinkSave");
        events.add("beforeDelete");
        events.add("afterDelete");
        EVENTS = Collections.unmodifiableList(events);
    }

    @Autowired
    ListableBeanFactory beanFactory;

    @Override
    public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
        super.configureValidatingRepositoryEventListener(validatingListener);
        Map<String, Validator> validators = beanFactory.getBeansOfType(Validator.class);
        for (Map.Entry<String, Validator> entry : validators.entrySet()) {
            EVENTS.stream().filter(p -> entry.getKey().startsWith(p)).findFirst()
                    .ifPresent(p -> validatingListener.addValidator(p, entry.getValue()));
        }
    }

}
Comment by jamlee [ 12/May/16 ]

it work for me . spring-data-rest 2.4

Comment by bitsofinfo [ 24/Jun/16 ]

Any progress on fixing doc or in the code?

Comment by Casey Link [ 28/Feb/18 ]

After debugging and fighting with this for way to long, I land here on this bug report

If the bug itself can't be fixed for whatever reasons, it would be nice at least to update the documentation

The docs say:

There are two ways to register a Validator instance in Spring Data REST: wire it by bean name or register the validator manually. For the majority of cases, the simple bean name prefix style will be sufficient.

That's exactly wrong!

Comment by Farrukh Najmi [ 06/Jun/18 ]

I am using springboot 2.0.1.RELEASE with spring-data-rest and followed the workaround mentioned here and my Validator is still not being invoked. Here are the details:

  • ValidatorRegistrar same as proposed
  • Validator class

 

@Component("beforeSaveBidValidator")
public class BeforeSaveBidValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Bid.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        Bid bid = (Bid)target;
        if (!bid.getAddendaAcknowledged()) {
            errors.rejectValue("addendaAcknowledged", 
                "addendaAcknowledged is not true");
        }
    }
}

 

  • Custom RestController for Bids

 

@RestController
@RequestMapping(path = "/bids")
@Api(value = "/bids", description = "CRUD operations with Bids")
public class BidController {

    private BidRepository bidRepository;

    @Autowired
    public BidController(
        BidRepository bidRepository) {
        this.bidRepository = bidRepository;
    }

    @PutMapping("{id}")
    public Bid update(@RequestBody @Valid Bid bid) {
        return bidRepository.save(bid);
    }
}

 

  • Rest Client code

 

Bid bid = new Bid()
...
bid.setAddendaAcknowledged(false)

Map<String, String> uriVariables = new HashMap<String, String>()
uriVariables.put("id", bid.id)

HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
HttpEntity<Bid> entity = new HttpEntity<>(bid, headers)
ResponseEntity<String> response = restTemplate.exchange(
        "/bids/{id}", HttpMethod.PUT, entity, Bid.class, bid.id)

// Expected: response.statusCode == HttpStatus.BAD_REQUEST
// Found: response.statusCode == HttpStatus.OK
// Debugger showed that Validator was never invoked.

 

Any idea what I am missing?

 

Comment by Eddie Bush [ 01/Oct/18 ]

What's the current work-around for this?

Comment by Rafael Renan Pacheco [ 01/May/19 ]

The workaround is blogged here: https://www.baeldung.com/spring-data-rest-validators

You set the events you want to register in the events array, like "beforeCreate", and the code will look for all validators the starts with this string and register it. This way you can create your validators as components, like this:

@Component
public class BeforeCreateItemValidator implements Validator {

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

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required");
    }
}

And the workaround to load all validators that begins with "beforeCreate" is this:

@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()));
        }
    }
}

If you add all possible events in the "events" array, you will get what Spring Data Rest should have been doing in the first place.

Generated at Sat Jul 20 05:52:11 UTC 2019 using JIRA 7.9.2#79002-sha1:3bb15b68ecd99a30eb364c4c1a393359bcad6278.