diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index ad546153e5..094d9a9a71 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -1,6 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 org.springframework.data @@ -42,9 +42,9 @@ ${org.springframework.version.range} - commons-logging - commons-logging - + commons-logging + commons-logging + @@ -99,7 +99,7 @@ 1.0 true - + javax.enterprise @@ -108,21 +108,21 @@ provided true - + javax.el el-api ${cdi.version} test - + org.apache.openwebbeans.test cditest-owb ${webbeans.version} test - + javax.servlet servlet-api @@ -130,8 +130,23 @@ test + + + javax.validation + validation-api + 1.0.0.GA + compile + + + + org.hibernate + hibernate-validator + 4.2.0.Final + test + + - + @@ -158,5 +173,5 @@ - + diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/BeanNames.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/BeanNames.java index c1d21bf094..2c4ef69d53 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/BeanNames.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/BeanNames.java @@ -25,4 +25,5 @@ public abstract class BeanNames { static final String INDEX_HELPER = "indexCreationHelper"; static final String MONGO = "mongo"; static final String DB_FACTORY = "mongoDbFactory"; + static final String VALIDATING_EVENT_LISTENER = "validatingMongoEventListener"; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java index 5e007e34ce..6fe7c1af0b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java @@ -30,12 +30,8 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.RuntimeBeanReference; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.beans.factory.support.ManagedList; -import org.springframework.beans.factory.support.ManagedSet; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.support.*; import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -52,18 +48,23 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener; +import org.springframework.scheduling.config.AnnotationDrivenBeanDefinitionParser; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.w3c.dom.Element; /** * @author Jon Brisbin * @author Oliver Gierke + * @author Maciej Walkowiak */ public class MappingMongoConverterParser extends AbstractBeanDefinitionParser { - private static final String BASE_PACKAGE = "base-package"; + private static final boolean jsr303Present = ClassUtils.isPresent("javax.validation.Validator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); @Override protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) @@ -107,9 +108,50 @@ protected AbstractBeanDefinition parseInternal(Element element, ParserContext pa registry.registerBeanDefinition(INDEX_HELPER, indexHelperBuilder.getBeanDefinition()); } + BeanDefinition validatingMongoEventListener = potentiallyCreateValidatingMongoEventListener(element, parserContext); + + if (validatingMongoEventListener != null) { + registry.registerBeanDefinition(VALIDATING_EVENT_LISTENER, validatingMongoEventListener); + } + return converterBuilder.getBeanDefinition(); } + private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) { + String disableValidation = element.getAttribute("disable-validation"); + + BeanDefinition result = null; + + if (disableValidation == null || Boolean.valueOf(disableValidation) == Boolean.FALSE) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(); + + RuntimeBeanReference validator = getValidator(builder, parserContext); + + if (validator != null) { + builder.getRawBeanDefinition().setBeanClass(ValidatingMongoEventListener.class); + builder.addConstructorArgValue(validator); + + result = builder.getBeanDefinition(); + } + } + + return result; + } + + private RuntimeBeanReference getValidator(Object source, ParserContext parserContext) { + if (!jsr303Present) { + return null; + } + + RootBeanDefinition validatorDef = new RootBeanDefinition(LocalValidatorFactoryBean.class); + validatorDef.setSource(source); + validatorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String validatorName = parserContext.getReaderContext().registerWithGeneratedName(validatorDef); + parserContext.registerComponent(new BeanComponentDefinition(validatorDef, validatorName)); + + return new RuntimeBeanReference(validatorName); + } + private String potentiallyCreateMappingContext(Element element, ParserContext parserContext, BeanDefinition conversionsDefinition) { @@ -221,7 +263,7 @@ public BeanMetadataElement parseConverter(Element element, ParserContext parserC /** * {@link TypeFilter} that returns {@literal false} in case any of the given delegates matches. - * + * * @author Oliver Gierke */ private static class NegatingFilter implements TypeFilter { @@ -230,7 +272,7 @@ private static class NegatingFilter implements TypeFilter { /** * Creates a new {@link NegatingFilter} with the given delegates. - * + * * @param filters */ public NegatingFilter(TypeFilter... filters) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListener.java new file mode 100644 index 0000000000..5f9f8d817b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListener.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010 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 + * + * http://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.mongodb.core.mapping.event; + +import com.mongodb.DBObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; +import java.util.Set; + +/** + * javax.validation dependant entities validator. + * When it is registered as Spring component its automatically invoked before entities are saved in database. + * + * @author Maciej Walkowiak + */ +public class ValidatingMongoEventListener extends AbstractMongoEventListener { + private static final Logger LOG = LoggerFactory.getLogger(ValidatingMongoEventListener.class); + + private final Validator validator; + + public ValidatingMongoEventListener(Validator validator) { + this.validator = validator; + } + + @Override + public void onBeforeSave(Object source, DBObject dbo) { + LOG.debug("Validating object: {}", source); + + Set violations = validator.validate(source); + + if (violations.size() > 0) { + LOG.info("During object: {} validation violations found: {}", source, violations); + + throw new ConstraintViolationException((Set>) violations); + } + } +} diff --git a/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-1.0.xsd b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-1.0.xsd index 702c855b03..c706ba8b9c 100644 --- a/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-1.0.xsd +++ b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-1.0.xsd @@ -214,6 +214,13 @@ The base package in which to scan for entities annotated with @Document + + + + Disables JSR-303 validation on MongoDB documents before they are saved. By default it is set to false. + + + diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserIntegrationTests.java index 533b6ce973..65b078d59c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserIntegrationTests.java @@ -37,7 +37,7 @@ import com.mongodb.DBObject; /** - * Integration tests for {@link MongoParser}. + * Integration tests for {@link MappingMongoConverterParser}. * * @author Oliver Gierke */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserValidationIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserValidationIntegrationTests.java new file mode 100644 index 0000000000..13eaddc6dc --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserValidationIntegrationTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2010 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 + * + * http://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.mongodb.config; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.ClassPathResource; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +/** + * Integration test for creation of instance of {@link org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener} + * by defining <mongo:mapping-converter /> in context XML + * + * @author Maciej Walkowiak + */ +public class MappingMongoConverterParserValidationIntegrationTests { + private DefaultListableBeanFactory factory; + private BeanDefinitionReader reader; + + @Before + public void setUp() { + factory = new DefaultListableBeanFactory(); + reader = new XmlBeanDefinitionReader(factory); + } + + @Test + public void validatingEventListenerCreatedWithDefaultConfig() { + reader.loadBeanDefinitions(new ClassPathResource("namespace/converter-default.xml")); + + assertThat(factory.getBean(BeanNames.VALIDATING_EVENT_LISTENER), is(not(nullValue()))); + } + + @Test + public void validatingEventListenerCreatedWhenValidationEnabled() { + reader.loadBeanDefinitions(new ClassPathResource("namespace/converter-validation-enabled.xml")); + + assertThat(factory.getBean(BeanNames.VALIDATING_EVENT_LISTENER), is(not(nullValue()))); + } + + @Test(expected = NoSuchBeanDefinitionException.class) + public void validatingEventListenersIsNotCreatedWhenDisabled() { + reader.loadBeanDefinitions(new ClassPathResource("namespace/converter-validation-disabled.xml")); + + factory.getBean(BeanNames.VALIDATING_EVENT_LISTENER); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/User.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/User.java new file mode 100644 index 0000000000..099d5001cc --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/User.java @@ -0,0 +1,30 @@ +package org.springframework.data.mongodb.core.mapping.event; + +import javax.validation.constraints.Min; +import javax.validation.constraints.Size; + +/** + * Class used to test JSR-303 validation @{link org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener} + * + * @author Maciej Walkowiak + */ +public class User { + @Size(min = 10) + private String name; + + @Min(18) + private Integer age; + + public User(String name, Integer age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public Integer getAge() { + return age; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTest.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTest.java new file mode 100644 index 0000000000..f35ccb8b9d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTest.java @@ -0,0 +1,46 @@ +package org.springframework.data.mongodb.core.mapping.event; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.validation.ConstraintViolationException; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class ValidatingMongoEventListenerTest { + @Autowired + private MongoTemplate mongoTemplate; + + @Test + public void shouldThrowConstraintViolationException() { + //given + User user = new User("john", 17); + + try { + //when + mongoTemplate.save(user); + + //then + fail(); + } catch (ConstraintViolationException e) { + assertThat(e.getConstraintViolations().size(), equalTo(2)); + } + } + + @Test + public void shouldNotThrowAnyExceptions() { + //given + User user = new User("john smith", 18); + + //when & then + mongoTemplate.save(user); + } +} diff --git a/spring-data-mongodb/src/test/resources/namespace/converter-default.xml b/spring-data-mongodb/src/test/resources/namespace/converter-default.xml new file mode 100644 index 0000000000..76a4cb1b1e --- /dev/null +++ b/spring-data-mongodb/src/test/resources/namespace/converter-default.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/spring-data-mongodb/src/test/resources/namespace/converter-validation-disabled.xml b/spring-data-mongodb/src/test/resources/namespace/converter-validation-disabled.xml new file mode 100644 index 0000000000..a8035d5719 --- /dev/null +++ b/spring-data-mongodb/src/test/resources/namespace/converter-validation-disabled.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/spring-data-mongodb/src/test/resources/namespace/converter-validation-enabled.xml b/spring-data-mongodb/src/test/resources/namespace/converter-validation-enabled.xml new file mode 100644 index 0000000000..0c97b7f7d3 --- /dev/null +++ b/spring-data-mongodb/src/test/resources/namespace/converter-validation-enabled.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTest-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTest-context.xml new file mode 100644 index 0000000000..1e54713b16 --- /dev/null +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListenerTest-context.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/spring-data-mongodb/template.mf b/spring-data-mongodb/template.mf index ae1431b311..c0ead7cc56 100644 --- a/spring-data-mongodb/template.mf +++ b/spring-data-mongodb/template.mf @@ -10,6 +10,7 @@ Import-Template: com.mongodb.*;version="0", com.mysema.query.*;version="[2.1.1, 3.0.0)";resolution:=optional, javax.annotation.processing.*;version="0", + javax.validation.*;version="1.0.0.GA", javax.enterprise.*;version="${cdi.version:[=.=.=,+1.0.0)}";resolution:=optional, javax.tools.*;version="0", org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional,