Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validator auto discovery not working for Spring Data Rest [DATAREST-524] #898

Open
spring-projects-issues opened this issue Apr 22, 2015 · 14 comments
Assignees
Labels

Comments

@spring-projects-issues
Copy link

@spring-projects-issues spring-projects-issues commented Apr 22, 2015

Daniel Moses opened DATAREST-524 and commented

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


34 votes, 37 watchers

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jul 13, 2015

Fabian Trampusch commented

Are there any news on this one?

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jul 13, 2015

Andreas Kluth commented

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

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jul 18, 2015

Fabian Trampusch commented

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?

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Apr 3, 2016

Sebastian Bathke commented

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

}

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented May 12, 2016

jamlee commented

it work for me . spring-data-rest 2.4

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 24, 2016

bitsofinfo commented

Any progress on fixing doc or in the code?

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Feb 28, 2018

Casey Link commented

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!

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 6, 2018

Farrukh Najmi commented

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?

 

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Oct 1, 2018

Eddie Bush commented

What's the current work-around for this?

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented May 1, 2019

Rafael Renan Pacheco commented

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

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Mar 18, 2020

kwix commented

Has anyone tried the workaround yet? I tried using the workaround but the events are not getting picked up. Would appreciate anyone's input on what might be causing this issue

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Apr 18, 2020

Servan Fichet commented

Yes it is working for me!

I added the configuration class and the Validator has been picked up.

Do not forget to annotate the validator class with @Component("beforeCreateItemValidator") and it should work.

Do you know if the bug has been fixed?

@sullrich84
Copy link

@sullrich84 sullrich84 commented Jun 12, 2021

The above-mentioned code solved the issue on my side. I slightly modified it to also register JSR 380 bean validators:

/**
 * Configuration to merge multiple validator concepts.
 *
 * @author Sebastian Ullrich
 * @since 1.0.0
 */
@Log4j2
@Configuration
@RequiredArgsConstructor
public class ValidatorConfig implements InitializingBean
{
    public static final String BEFORE_CREATE = "beforeCreate";
    public static final String BEFORE_SAVE = "beforeSave";

    private final Map<String, Validator> validators;
    private final LocalValidatorFactoryBean beanValidator;
    private final ValidatingRepositoryEventListener validatingListener;

    /**
     * Assigns all present {@link org.springframework.validation.Validator Validators}
     * to the {@link ValidatingRepositoryEventListener}.
     *
     * @see <a href="https://jira.spring.io/browse/DATAREST-524">DATAREST-524</a>
     */
    @Override
    public void afterPropertiesSet ()
    {
        // Assign custom validators
        validators.entrySet().stream()
            .filter(entry -> entry.getKey().startsWith(BEFORE_CREATE))
            .map(Map.Entry::getValue)
            .forEach(validator -> validatingListener.addValidator(BEFORE_CREATE, validator));

        // Assign BeanValidator (JSR 380)
        validatingListener.addValidator(BEFORE_CREATE, beanValidator);
        validatingListener.addValidator(BEFORE_SAVE, beanValidator);
    }
}

@julianvargkim
Copy link

@julianvargkim julianvargkim commented Nov 6, 2021

This is still an issue as of 2021, according to baeldung, you can do:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants