spring-projects / spring-data-rest

Simplifies building hypermedia-driven REST web services on top of Spring Data repositories
https://spring.io/projects/spring-data-rest
Apache License 2.0
914 stars 559 forks source link

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

Open spring-projects-issues opened 9 years ago

spring-projects-issues commented 9 years ago

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 commented 9 years ago

Fabian Trampusch commented

Are there any news on this one?

spring-projects-issues commented 9 years ago

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 commented 9 years ago

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 commented 8 years ago

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 commented 8 years ago

jamlee commented

it work for me . spring-data-rest 2.4

spring-projects-issues commented 8 years ago

bitsofinfo commented

Any progress on fixing doc or in the code?

spring-projects-issues commented 6 years ago

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 commented 6 years ago

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:

 

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

 

 

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

 

 

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 commented 5 years ago

Eddie Bush commented

What's the current work-around for this?

spring-projects-issues commented 5 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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);
    }
}
alrightjulian commented 2 years ago

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()));
        }
    }
}
lfe135 commented 3 weeks ago

In 2024, https://docs.spring.io/spring-data/rest/reference/validation.html mentioned auto wired still not work.