/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.test.internal.engine.cascaded;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertCorrectPropertyPaths;
import java.lang.reflect.TypeVariable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ConstraintDeclarationException;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.Size;
import javax.validation.valueextraction.ExtractedValue;
import javax.validation.valueextraction.ValueExtractor;
import javax.validation.valueextraction.ValueExtractorDeclarationException;
import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.internal.engine.cascading.AnnotatedObject;
import org.hibernate.validator.internal.engine.cascading.ValueExtractorDescriptor;
import org.hibernate.validator.internal.util.TypeHelper;
import org.hibernate.validator.testutil.TestForIssue;
import org.hibernate.validator.testutil.ValidationXmlTestHelper;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.base.Optional;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
/**
* @author Gunnar Morling
*/
public class CustomValueExtractorTest {
private ValidationXmlTestHelper validationXmlTestHelper;
@BeforeMethod
public void setupValidationXmlTestHelper() {
validationXmlTestHelper = new ValidationXmlTestHelper( getClass() );
}
@Test
public void canUseCustomExtractor() throws Exception {
Cinema cinema = new Cinema();
cinema.visitor = new SomeReference<>( new Visitor() );
Validator validator = Validation.byProvider( HibernateValidator.class )
.configure()
.addValueExtractor( new ReferenceValueExtractor() )
.buildValidatorFactory()
.getValidator();
Set<ConstraintViolation<Cinema>> violations = validator.validate( cinema );
assertCorrectPropertyPaths( violations, "visitor.name" );
}
@Test
public void canUseCustomValueExtractorForMultimaps() throws Exception {
CustomerWithMultimap bob = new CustomerWithMultimap();
Validator validator = Validation.byProvider( HibernateValidator.class )
.configure()
.addValueExtractor( new MultimapValueExtractor() )
.buildValidatorFactory()
.getValidator();
Set<ConstraintViolation<CustomerWithMultimap>> violations = validator.validate( bob );
assertCorrectPropertyPaths( violations, "addressByType<V>[work].email", "addressByType<V>[work].email" );
}
@Test
@TestForIssue(jiraKey = "HV-1260")
public void canUseValueExtractorGivenInValidationXml() {
validationXmlTestHelper.runWithCustomValidationXml(
"value-extractor-validation.xml", () -> {
CustomerWithMultimap bob = new CustomerWithMultimap();
Validator validator = Validation.buildDefaultValidatorFactory()
.getValidator();
Set<ConstraintViolation<CustomerWithMultimap>> violations = validator.validate( bob );
assertCorrectPropertyPaths( violations, "addressByType<V>[work].email", "addressByType<V>[work].email" );
} );
}
@Test
public void canUseCustomValueExtractorPerValidatorForMultimaps() throws Exception {
CustomerWithStringStringMultimap bob = new CustomerWithStringStringMultimap();
Validator validator = Validation.buildDefaultValidatorFactory()
.usingContext()
.addValueExtractor( new MultimapValueExtractor() )
.getValidator();
Set<ConstraintViolation<CustomerWithStringStringMultimap>> violations = validator.validate( bob );
assertCorrectPropertyPaths( violations, "addressByType<V>[work].multimap_value", "addressByType<V>[work].multimap_value" );
}
@Test
@TestForIssue(jiraKey = "HV-1261")
public void canUseValueExtractorGivenViaServiceLoader() {
CustomerWithOptionalAddress bob = new CustomerWithOptionalAddress();
Validator validator = Validation.buildDefaultValidatorFactory()
.getValidator();
Set<ConstraintViolation<CustomerWithOptionalAddress>> violations = validator.validate( bob );
assertCorrectPropertyPaths( violations, "address" );
}
@Test(expectedExceptions = ConstraintDeclarationException.class, expectedExceptionsMessageRegExp = "HV000197.*")
public void missingCustomExtractorThrowsException() throws Exception {
Cinema cinema = new Cinema();
cinema.visitor = new SomeReference<>( new Visitor() );
Validator validator = Validation.byProvider( HibernateValidator.class )
.configure()
.buildValidatorFactory()
.getValidator();
validator.validate( cinema );
}
@Test
public void valueExtractorPrecedenceIsAppliedCorrectly() {
validationXmlTestHelper.runWithCustomValidationXml(
"value-extractor-validation.xml",
() -> {
CustomerWithOptionalAddress bob = new CustomerWithOptionalAddress();
Validator validator = Validation.buildDefaultValidatorFactory()
.getValidator();
Set<ConstraintViolation<CustomerWithOptionalAddress>> violations = validator.validate( bob );
// validation.xml overrides service loader
assertCorrectPropertyPaths( violations, "address.1" );
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
.configure()
.addValueExtractor( new GuavaOptionalValueExtractor2() )
.buildValidatorFactory();
validator = validatorFactory.getValidator();
violations = validator.validate( bob );
// VF overrides validation.xml
assertCorrectPropertyPaths( violations, "address.2" );
validator = validatorFactory.usingContext()
.addValueExtractor( new GuavaOptionalValueExtractor3() )
.getValidator();
violations = validator.validate( bob );
// V overrides VF
assertCorrectPropertyPaths( violations, "address.3" );
}
);
}
@Test
public void canObtainDefaultExtractors() {
HibernateValidatorConfiguration config = Validation.byProvider( HibernateValidator.class )
.configure();
Set<ValueExtractor<?>> defaultExtractors = config.getDefaultValueExtractors();
assertThat( defaultExtractors ).isNotEmpty();
Set<TypeVariable<?>> mapExtractors = defaultExtractors.stream()
.filter( e -> {
return TypeHelper.getErasedReferenceType( new ValueExtractorDescriptor( e ).getExtractedType() ) == Map.class;
} )
.map( e -> new ValueExtractorDescriptor( e ).getExtractedTypeParameter() )
.collect( Collectors.toSet() );
assertThat( mapExtractors ).containsOnly(
Map.class.getTypeParameters()[0], Map.class.getTypeParameters()[1], AnnotatedObject.INSTANCE );
Set<TypeVariable<?>> optionalExtractors = defaultExtractors.stream()
.filter( e -> {
return TypeHelper.getErasedReferenceType( new ValueExtractorDescriptor( e ).getExtractedType() ) == java.util.Optional.class;
} )
.map( e -> new ValueExtractorDescriptor( e ).getExtractedTypeParameter() )
.collect( Collectors.toSet() );
assertThat( optionalExtractors ).describedAs( "Expecting extractor for <T>, but not the legacy extractor for java.util.Optional" )
. containsOnly( java.util.Optional.class.getTypeParameters()
);
Set<TypeVariable<?>> guavaOptionalExtractors = defaultExtractors.stream()
.filter( e -> {
return TypeHelper.getErasedReferenceType( new ValueExtractorDescriptor( e ).getExtractedType() ) == Optional.class;
} )
.map( e -> new ValueExtractorDescriptor( e ).getExtractedTypeParameter() )
.collect( Collectors.toSet() );
assertThat( guavaOptionalExtractors ).describedAs( "Extractor for Guava's Optional shouldn't be part of default extractors" )
.isEmpty();
}
@Test(expectedExceptions = ValueExtractorDeclarationException.class, expectedExceptionsMessageRegExp = "HV000208.*")
public void configuringMultipleExtractorsForSameTypeAndTypeUseCausesException() throws Exception {
Validation.byDefaultProvider()
.configure()
.addValueExtractor( new GuavaOptionalValueExtractor1() )
.addValueExtractor( new GuavaOptionalValueExtractor1() );
}
@Test(expectedExceptions = ValueExtractorDeclarationException.class, expectedExceptionsMessageRegExp = "HV000208.*")
public void configuringValidatorWithMultipleExtractorsForSameTypeAndTypeUseCausesException() throws Exception {
Validation.buildDefaultValidatorFactory()
.usingContext()
.addValueExtractor( new GuavaOptionalValueExtractor1() )
.addValueExtractor( new GuavaOptionalValueExtractor1() );
}
@Test(expectedExceptions = ValueExtractorDeclarationException.class, expectedExceptionsMessageRegExp = "HV000208.*")
public void configuringMultipleExtractorsForSameTypeAndTypeUseInValidationXmlCausesException() throws Exception {
validationXmlTestHelper.runWithCustomValidationXml(
"multiple-value-extractors-for-same-type-and-type-use-validation.xml",
() -> Validation.buildDefaultValidatorFactory() );
}
private static class CustomerWithMultimap {
ListMultimap<String, @Valid EmailAddress> addressByType;
public CustomerWithMultimap() {
addressByType = ArrayListMultimap.create();
addressByType.put( "work", new EmailAddress( "work@bob.de" ) );
addressByType.put( "work", new EmailAddress( "invalid" ) );
addressByType.put( "work", new EmailAddress( "alsoinvalid" ) );
addressByType.put( "home", new EmailAddress( "home@bob.de" ) );
}
}
private static class CustomerWithStringStringMultimap {
ListMultimap<String, @Email String> addressByType;
public CustomerWithStringStringMultimap() {
addressByType = ArrayListMultimap.create();
addressByType.put( "work", "work@bob.de" );
addressByType.put( "work", "invalid" );
addressByType.put( "work", "alsoinvalid" );
addressByType.put( "home", "home@bob.de" );
}
}
private static class CustomerWithOptionalAddress {
Optional<@Size(min = 5) String> address = Optional.of( "aaa" );
}
public static class CustomMultimapValueExtractor implements ValueExtractor<Multimap<?, @ExtractedValue ?>> {
@Override
public void extractValues(Multimap<?, ?> originalValue, ValueExtractor.ValueReceiver receiver) {
for ( Entry<?, ?> entry : originalValue.entries() ) {
receiver.keyedValue( "multimap_value_1", entry.getKey(), entry.getValue() );
}
}
}
public static class GuavaOptionalValueExtractor1 implements ValueExtractor<Optional<@ExtractedValue ?>> {
@Override
public void extractValues(Optional<@ExtractedValue ?> originalValue, ValueExtractor.ValueReceiver receiver) {
receiver.value( "1", originalValue.isPresent() ? originalValue.get() : null );
}
}
public static class GuavaOptionalValueExtractor2 implements ValueExtractor<Optional<@ExtractedValue ?>> {
@Override
public void extractValues(Optional<@ExtractedValue ?> originalValue, ValueExtractor.ValueReceiver receiver) {
receiver.value( "2", originalValue.isPresent() ? originalValue.get() : null );
}
}
public static class GuavaOptionalValueExtractor3 implements ValueExtractor<Optional<@ExtractedValue ?>> {
@Override
public void extractValues(Optional<@ExtractedValue ?> originalValue, ValueExtractor.ValueReceiver receiver) {
receiver.value( "3", originalValue.isPresent() ? originalValue.get() : null );
}
}
}