From bb660d5e5c84cde1a4bded1248c1a22d81c82625 Mon Sep 17 00:00:00 2001 From: Eleftherios Laskaridis Date: Wed, 28 Jul 2021 18:52:21 +0300 Subject: [PATCH] DATAREST-524 Invoke validators registered with composite bean names The ValidatingRespositoryEventListener will currently ignore any validator that has been registered under a composite key consisting of the event name post-fixed by the model name and the keyword "Validator". This does not allow executing validator beans registered using the "@Component" or "@Bean" annotations, which should be possible according to the reference guide. This happens because the code will only look for validators registered using the event name as a key only. To fix this, I added a second lookup (if the previous fails) by a composite key name that follows the semantics described previously. See: https://docs.spring.io/spring-data/rest/docs/3.5.3/reference/html/#validation --- .../ValidatingRepositoryEventListener.java | 26 +- ...atingRepositoryEventListenerUnitTests.java | 314 ++++++++++++++++++ 2 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 spring-data-rest-core/src/test/java/org/springframework/data/rest/core/event/ValidatingRepositoryEventListenerUnitTests.java diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/event/ValidatingRepositoryEventListener.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/event/ValidatingRepositoryEventListener.java index 736d4ef6b7..df7d8c0d89 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/event/ValidatingRepositoryEventListener.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/event/ValidatingRepositoryEventListener.java @@ -15,10 +15,7 @@ */ package org.springframework.data.rest.core.event; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; +import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,12 +30,15 @@ import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; +import static java.util.Collections.emptySet; + /** * {@link org.springframework.context.ApplicationListener} implementation that dispatches {@link RepositoryEvent}s to a * specific {@link Validator}. * * @author Jon Brisbin * @author Oliver Gierke + * @author Eleftherios Laskaridis */ public class ValidatingRepositoryEventListener extends AbstractRepositoryEventListener { @@ -167,7 +167,7 @@ private Errors validate(String event, Object entity) { Errors errors = new ValidationErrors(entity, persistentEntitiesFactory.getObject()); - for (Validator validator : getValidatorsForEvent(event)) { + for (Validator validator : getValidatorsForEvent(event, entity)) { if (validator.supports(entity.getClass())) { LOGGER.debug("{}: {} with {}", event, entity, validator); @@ -182,9 +182,19 @@ private Errors validate(String event, Object entity) { return errors; } - private Collection getValidatorsForEvent(String event) { + private Collection getValidatorsForEvent(String eventName, Object validationTarget) { + Collection validators = this.validators.get(eventName); + // Allows registering validators by a composite key name consisting of the event name, + // post-fixed by the model name and the keyword "Validator". Useful when registering + // validators as "@Component"s or "@Bean"s (which both require a unique bean name). + if (validators == null || validators.isEmpty()) { + String compositeValidatorKeyName = createCompositeValidatorKeyNameFor(eventName, validationTarget); + validators = this.validators.get(compositeValidatorKeyName); + } + return Optional.ofNullable(validators).orElse(emptySet()); + } - Collection validators = this.validators.get(event); - return validators == null ? Collections. emptySet() : validators; + private String createCompositeValidatorKeyNameFor(String eventName, Object validationTarget) { + return eventName + validationTarget.getClass().getSimpleName() + "Validator"; } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/event/ValidatingRepositoryEventListenerUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/event/ValidatingRepositoryEventListenerUnitTests.java new file mode 100644 index 0000000000..c312d4ab82 --- /dev/null +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/event/ValidatingRepositoryEventListenerUnitTests.java @@ -0,0 +1,314 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.event; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ValidatingRepositoryEventListener} + * + * @author Elefhterios Laskaridis + */ +@RunWith(MockitoJUnitRunner.class) +public class ValidatingRepositoryEventListenerUnitTests { + + private ValidatingRepositoryEventListener subject; + + @Mock private ObjectFactory persistentEntitiesObjectFactory; + @Mock private PersistentEntities persistentEntities; + + @Before + public void setUp() { + when(persistentEntitiesObjectFactory.getObject()).thenReturn(persistentEntities); + this.subject = new ValidatingRepositoryEventListener(this.persistentEntitiesObjectFactory); + } + + private static final class StubModel { } + + private static final class StubModelValidator implements Validator { + + private Object validationTarget; + + public boolean isInvoked() { + return this.validationTarget != null; + } + + public Object getValidationTarget() { + return validationTarget; + } + + @Override + public boolean supports(Class aClass) { + return StubModel.class.equals(aClass); + } + + @Override + public void validate(Object o, Errors errors) { + this.validationTarget = o; + } + } + + @Test // DATAREST-524 + public void invokesAfterCreateValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterCreate", validator); + this.subject.onAfterCreate(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesAfterDeleteValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterDelete", validator); + this.subject.onAfterDelete(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesAfterDeleteValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterDeleteStubModelValidator", validator); + this.subject.onAfterDelete(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesAfterCreateValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterCreateStubModelValidator", validator); + this.subject.onAfterCreate(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void doesNotInvokeAfterLinkDeleteValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterLinkDelete", validator); + this.subject.onAfterLinkDelete(validationTarget, null); + + assertFalse(validator.isInvoked()); + assertNull(validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesAfterLinkDeleteValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterLinkDeleteStubModelValidator", validator); + this.subject.onAfterLinkDelete(validationTarget, null); + + assertFalse(validator.isInvoked()); + assertNull(validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesAfterLinkSaveValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterLinkSave", validator); + this.subject.onAfterLinkSave(validationTarget, null); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesAfterLinkSaveValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterLinkSaveStubModelValidator", validator); + this.subject.onAfterLinkSave(validationTarget, null); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesAfterSaveValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterSave", validator); + this.subject.onAfterSave(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesAfterSaveValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("afterSaveStubModelValidator", validator); + this.subject.onAfterSave(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeCreateValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeCreate", validator); + this.subject.onBeforeCreate(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeCreateValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeCreateStubModelValidator", validator); + this.subject.onBeforeCreate(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeDeleteValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeDelete", validator); + this.subject.onBeforeDelete(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeDeleteValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeDeleteStubModelValidator", validator); + this.subject.onBeforeDelete(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeLinkDeleteValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeLinkDelete", validator); + this.subject.onBeforeLinkDelete(validationTarget, null); + + assertFalse(validator.isInvoked()); + assertNull(validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeLinkDeleteValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeLinkDeleteStubModelValidator", validator); + this.subject.onBeforeLinkDelete(validationTarget, null); + + assertFalse(validator.isInvoked()); + assertNull(validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeLinkSaveValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeLinkSave", validator); + this.subject.onBeforeLinkSave(validationTarget, null); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeLinkSaveValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeLinkSaveStubModelValidator", validator); + this.subject.onBeforeLinkSave(validationTarget, null); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeSaveValidator_whenValidatorIsRegisteredByEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeSave", validator); + this.subject.onBeforeSave(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } + + @Test // DATAREST-524 + public void invokesBeforeSaveValidator_whenValidatorIsRegisteredByCompositeEventName() { + StubModelValidator validator = new StubModelValidator(); + StubModel validationTarget = new StubModel(); + + this.subject.addValidator("beforeSaveStubModelValidator", validator); + this.subject.onBeforeSave(validationTarget); + + assertTrue(validator.isInvoked()); + assertEquals(validationTarget, validator.getValidationTarget()); + } +}