Skip to content

Commit

Permalink
HV-1339 Add support for non parameterized type in value extraction
Browse files Browse the repository at this point in the history
  • Loading branch information
gsmet committed May 16, 2017
1 parent e243ab2 commit 9f5983d
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 5 deletions.
@@ -0,0 +1,27 @@
/*
* 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.internal.engine.cascading;

import java.util.OptionalDouble;

import javax.validation.valueextraction.ExtractedValue;
import javax.validation.valueextraction.UnwrapByDefault;
import javax.validation.valueextraction.ValueExtractor;

/**
* @author Guillaume Smet
*/
@UnwrapByDefault
public class OptionalDoubleValueExtractor implements ValueExtractor<@ExtractedValue(type = Double.class) OptionalDouble> {

static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor( new OptionalDoubleValueExtractor() );

@Override
public void extractValues(OptionalDouble originalValue, ValueReceiver receiver) {
receiver.value( null, originalValue.isPresent() ? originalValue.getAsDouble() : null );
}
}
@@ -0,0 +1,27 @@
/*
* 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.internal.engine.cascading;

import java.util.OptionalInt;

import javax.validation.valueextraction.ExtractedValue;
import javax.validation.valueextraction.UnwrapByDefault;
import javax.validation.valueextraction.ValueExtractor;

/**
* @author Guillaume Smet
*/
@UnwrapByDefault
public class OptionalIntValueExtractor implements ValueExtractor<@ExtractedValue(type = Integer.class) OptionalInt> {

static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor( new OptionalIntValueExtractor() );

@Override
public void extractValues(OptionalInt originalValue, ValueReceiver receiver) {
receiver.value( null, originalValue.isPresent() ? originalValue.getAsInt() : null );
}
}
@@ -0,0 +1,27 @@
/*
* 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.internal.engine.cascading;

import java.util.OptionalLong;

import javax.validation.valueextraction.ExtractedValue;
import javax.validation.valueextraction.UnwrapByDefault;
import javax.validation.valueextraction.ValueExtractor;

/**
* @author Guillaume Smet
*/
@UnwrapByDefault
public class OptionalLongValueExtractor implements ValueExtractor<@ExtractedValue(type = Long.class) OptionalLong> {

static final ValueExtractorDescriptor DESCRIPTOR = new ValueExtractorDescriptor( new OptionalLongValueExtractor() );

@Override
public void extractValues(OptionalLong originalValue, ValueReceiver receiver) {
receiver.value( null, originalValue.isPresent() ? originalValue.getAsLong() : null );
}
}
Expand Up @@ -13,6 +13,7 @@
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import javax.validation.valueextraction.ExtractedValue;
import javax.validation.valueextraction.UnwrapByDefault;
Expand All @@ -37,6 +38,7 @@ public class ValueExtractorDescriptor {
private final Key key;
private final ValueExtractor<?> valueExtractor;
private final boolean unwrapByDefault;
private final Optional<Class<?>> extractedType;

public ValueExtractorDescriptor(ValueExtractor<?> valueExtractor) {
AnnotatedParameterizedType valueExtractorDefinition = getValueExtractorDefinition( valueExtractor.getClass() );
Expand All @@ -47,6 +49,7 @@ public ValueExtractorDescriptor(ValueExtractor<?> valueExtractor) {
);
this.valueExtractor = valueExtractor;
this.unwrapByDefault = hasUnwrapByDefaultAnnotation( valueExtractor.getClass() );
this.extractedType = getExtractedType( valueExtractorDefinition );
}

@SuppressWarnings("rawtypes")
Expand Down Expand Up @@ -74,6 +77,9 @@ private static TypeVariable<?> getExtractedTypeParameter(AnnotatedParameterizedT
if ( extractedTypeParameter != null ) {
throw LOG.getValueExtractorDeclaresExtractedValueMultipleTimesException( extractorImplementationType );
}
if ( !Void.class.equals( typeArgument.getAnnotation( ExtractedValue.class ).type() ) ) {
throw LOG.extractedValueOnTypeParameterOfContainerTypeMayNotDefineTypeAttribute( extractorImplementationType );
}

extractedTypeParameter = containerTypeRaw.getTypeParameters()[i];
}
Expand All @@ -88,6 +94,19 @@ private static TypeVariable<?> getExtractedTypeParameter(AnnotatedParameterizedT
return extractedTypeParameter;
}

private static Optional<Class<?>> getExtractedType(AnnotatedParameterizedType valueExtractorDefinition) {
AnnotatedType containerType = valueExtractorDefinition.getAnnotatedActualTypeArguments()[0];

if ( containerType.isAnnotationPresent( ExtractedValue.class ) ) {
Class<?> extractedType = containerType.getAnnotation( ExtractedValue.class ).type();
if ( !Void.class.equals( extractedType ) ) {
return Optional.of( ReflectionHelper.boxedType( extractedType ) );
}
}

return Optional.empty();
}

@SuppressWarnings("rawtypes")
private static Class<?> getContainerType(AnnotatedParameterizedType valueExtractorDefinition, Class<? extends ValueExtractor> extractorImplementationType) {
AnnotatedType containerType = valueExtractorDefinition.getAnnotatedActualTypeArguments()[0];
Expand Down Expand Up @@ -147,6 +166,10 @@ public TypeVariable<?> getExtractedTypeParameter() {
return key.extractedTypeParameter;
}

public Optional<Class<?>> getExtractedType() {
return extractedType;
}

public ValueExtractor<?> getValueExtractor() {
return valueExtractor;
}
Expand Down
Expand Up @@ -79,6 +79,9 @@ public class ValueExtractorManager {
specDefinedExtractors.add( IterableValueExtractor.DESCRIPTOR );

specDefinedExtractors.add( OptionalValueExtractor.DESCRIPTOR );
specDefinedExtractors.add( OptionalIntValueExtractor.DESCRIPTOR );
specDefinedExtractors.add( OptionalDoubleValueExtractor.DESCRIPTOR );
specDefinedExtractors.add( OptionalLongValueExtractor.DESCRIPTOR );

specDefinedExtractors.add( ObjectValueExtractor.DESCRIPTOR );

Expand Down
Expand Up @@ -86,9 +86,14 @@ private static <A extends Annotation> Type addValueExtractorDescriptorForWrapped

valueExtractionPath.add( TypeParameterAndExtractor.of( valueExtractorDescriptorCandidate ) );

return getSingleTypeParameterBind( typeResolutionHelper,
location.getTypeForValidatorResolution(),
valueExtractorDescriptorCandidate.getContainerType() );
if ( valueExtractorDescriptorCandidate.getExtractedType().isPresent() ) {
return valueExtractorDescriptorCandidate.getExtractedType().get();
}
else {
return getSingleTypeParameterBind( typeResolutionHelper,
location.getTypeForValidatorResolution(),
valueExtractorDescriptorCandidate.getContainerType() );
}
}
}

Expand Down
Expand Up @@ -433,7 +433,7 @@ public static Object getMappedValue(Object value, Object key) {
* @throws IllegalArgumentException in case the parameter {@code primitiveType} does not
* represent a primitive type.
*/
public static Class<?> boxedType(Class<?> primitiveType) {
private static Class<?> internalBoxedType(Class<?> primitiveType) {
Class<?> wrapperType = PRIMITIVE_TO_WRAPPER_TYPES.get( primitiveType );

if ( wrapperType == null ) {
Expand All @@ -448,7 +448,19 @@ public static Class<?> boxedType(Class<?> primitiveType) {
*/
public static Type boxedType(Type type) {
if ( type instanceof Class && ( (Class<?>) type ).isPrimitive() ) {
return boxedType( (Class<?>) type );
return internalBoxedType( (Class<?>) type );
}
else {
return type;
}
}

/**
* Returns the corresponding auto-boxed type if given a primitive type. Returns the given type itself otherwise.
*/
public static Class<?> boxedType(Class<?> type) {
if ( type.isPrimitive() ) {
return internalBoxedType( (Class<?>) type );
}
else {
return type;
Expand Down
Expand Up @@ -757,4 +757,9 @@ ValidationException getContainerElementTypeHasAlreadyBeenConfiguredViaXmlMapping
@Message(id = 219, value = "Unable to get the most specific value extractor for type %1$s as several value extractors are defined in this type's parallel hierarchies: %2$s.")
ConstraintDeclarationException unableToGetMostSpecificValueExtractorDueToSeveralValueExtractorsDefinedForParallelHierarchies(@FormatWith(ClassObjectFormatter.class) Class<?> valueType,
@FormatWith(CollectionOfClassesObjectFormatter.class) Collection<Class<? extends ValueExtractor>> valueExtractors);

@SuppressWarnings("rawtypes")
@Message(id = 220, value = "When @ExtractedValue is defined on a type parameter of a container type, the type attribute may not be set: %1$s.")
ValueExtractorDefinitionException extractedValueOnTypeParameterOfContainerTypeMayNotDefineTypeAttribute(
@FormatWith(ClassObjectFormatter.class) Class<? extends ValueExtractor> extractorImplementationType);
}
@@ -0,0 +1,182 @@
/*
* 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.valuehandling;

import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertCorrectConstraintTypes;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertNumberOfViolations;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.assertThat;
import static org.hibernate.validator.testutil.ConstraintViolationAssert.pathWith;

import java.util.HashMap;
import java.util.Map;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.valueextraction.ExtractedValue;
import javax.validation.valueextraction.UnwrapByDefault;
import javax.validation.valueextraction.ValueExtractor;

import org.hibernate.validator.HibernateValidator;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

/**
* @author Guillaume Smet
*/
public class NonGenericContainerValueExtractorTest {

private Validator validator;

@BeforeClass
public void setup() {
validator = Validation.byProvider( HibernateValidator.class )
.configure()
.addValueExtractor( new FooContainerValueExtractor() )
.buildValidatorFactory()
.getValidator();
}

@Test
public void testValueExtraction() {
Set<ConstraintViolation<Bean>> constraintViolations = validator.validate( Bean.valid() );
assertNumberOfViolations( constraintViolations, 0 );

constraintViolations = validator.validate( Bean.nullFoo() );
assertNumberOfViolations( constraintViolations, 1 );
assertCorrectConstraintTypes( constraintViolations, NotNull.class );
assertThat( constraintViolations ).containsOnlyPaths(
pathWith()
.property( "map" )
.containerElement( "<map value>", true, "key", null, Map.class, 1 )
);

// check that cascaded validation is working correctly
constraintViolations = validator.validate( Bean.invalid() );
assertNumberOfViolations( constraintViolations, 3 );
assertCorrectConstraintTypes( constraintViolations, Min.class, NotBlank.class, Max.class );
assertThat( constraintViolations ).containsOnlyPaths(
pathWith()
.property( "map" )
.property( "foo", true, "key", null, Map.class, 1 )
.property( "property" ),
pathWith()
.property( "map" )
.property( "foo", true, "key", null, Map.class, 1 )
.property( "optionalInt" ),
pathWith()
.property( "map" )
.property( "foo", true, "key", null, Map.class, 1 )
.property( "bar" )
.property( "optionalLong" )
);
}

@UnwrapByDefault
private static class FooContainerValueExtractor implements ValueExtractor<@ExtractedValue(type = Foo.class) FooContainer> {

@Override
public void extractValues(FooContainer originalValue, ValueReceiver receiver) {
receiver.value( null, originalValue.get() );
}
}

private static class Bean {

private final Map<String, @NotNull @Valid FooContainer> map = new HashMap<>();

private Bean(FooContainer container) {
map.put( "key", container );
}

private static Bean valid() {
return new Bean( FooContainer.valid() );
}

private static Bean invalid() {
return new Bean( FooContainer.invalid() );
}

private static Bean nullFoo() {
return new Bean( new FooContainer( null ) );
}
}

private static class FooContainer {

@Valid
private final Foo foo;

private FooContainer(Foo foo) {
this.foo = foo;
}

public Foo get() {
return foo;
}

private static FooContainer valid() {
return new FooContainer( Foo.valid() );
}

private static FooContainer invalid() {
return new FooContainer( Foo.invalid() );
}
}

private static class Foo {

@NotBlank
private final String property;

@Min(value = 5)
private final OptionalInt optionalInt;

@Valid
private Bar bar;

private Foo(String property, OptionalInt optionalInt, Bar bar) {
this.property = property;
this.optionalInt = optionalInt;
this.bar = bar;
}

private static Foo valid() {
return new Foo(
"value",
OptionalInt.of( 6 ),
new Bar( OptionalLong.of( 6 ) )
);
}

private static Foo invalid() {
return new Foo(
"",
OptionalInt.of( 4 ),
new Bar( OptionalLong.of( 18 ) )
);
}
}

private static class Bar {

@Max(value = 16)
private final OptionalLong optionalLong;

private Bar(OptionalLong optionalLong) {
this.optionalLong = optionalLong;
}
}
}

0 comments on commit 9f5983d

Please sign in to comment.