Appendix A: Value extraction for cascaded validation and type argument constraints (BVAL-508)
This appendix describes the current work-in-progress around the retrieval of values to be validated during cascaded validation (e.g. List<@Valid Order> orders
)
and evaluation of type argument constraints (e.g. Optional<@Email String> email
)
It is based on the original proposals for BVAL-508.
Motivation
Value extraction is needed when constraints are applied to the element(s) stored within a container type. There are two categories:
-
Cascaded validation of iterables, maps and arrays as triggered via
@Valid
:@Valid List<Order> orders
; in this case all the values stored in the list must be extracted so each can be validated -
Validation of type argument constraints as enabled by Java 8:
Optional<@Email String> email
orProperty<@Min(1) Integer> value
; in this case, e.g. the wrappedString
andInteger
values must be extracted so the@Email
and@Min
constraints can be applied
Both cases can also overlap: List<@RetailOrder @Valid Order> retailOrders
.
Type argument constraints and pluggable extractors will also make cascaded validation more flexible.
As of BV 1.1, there was a fixed set of data types supported by cascaded validation mandated by the spec (Iterable
, arrays, Map
(only the values would be validated).
This excludes use cases such as custom collection types (e.g. Guava’s multi-map),
validation of map keys or collection types of other JVM languages (such as Ceylon’s collection framework).
Allowing to put @Valid
on type parameters allows to express the subject of cascaded validation more specifically:
Map<@Valid AddressType, @Valid Address> addresses
would describe that both, map keys and values, should be validated.
Requirements
-
Constraints can be applied to type arguments (of bean properties, but also executable parameters and method return values):
Property<@Min(1) Integer> value
-
Individual type parameters can be marked for cascaded validation:
Map<@Valid AddressType, Address> addresses
-
How values are retrieved must be customizable by means of a pluggable value extractor mechanism, with a set of defined default extractors
-
Sometimes constraints should apply to a value wrapped by a container type, but there is no type parameter to put the constraints to. JavaFX’s
Property
hierarchy is the most prominent example:@Email StringProperty emailProperty
. In such case the value should be implicitly extracted from the container if unambiguously doable. -
Allow to configure type argument constraints and cascades via XML mappings
-
Expose meta-data on type argument constraints and cascades in the constraint metadata API
Non-requirements
-
Constraints on type uses in local variables, invocations etc:
@NotNull String name = "Emmanuel";
,new @NonEmpty @Readonly List<String>(myNonEmptyStringSet)
-
constraints on type uses in type definitions:
class CustList extends List<@NotNull Customer> {
}
Solution
Value retrieval for cascaded validation and validation of type argument constraints is done via the ValueExtractor
API:
Unresolved directive in BVAL-508-appendix.asciidoc - include::{validation-api-source-dir}javax/validation/valueextraction/ValueExtractor.java[lines=7..8;15..-1]
An extractor is tied to one specific type parameter of the type from which it extracts values. The @ExtractedValue
annotation is used to mark that type parameter.
As an example, this is how the implementation of the list extractor may look like:
class ListValueExtractor implements ValueExtractor<List<@ExtractedValue ?>> {
@Override
public void extractValues(List<?> originalValue, ValueReceiver receiver) {
for ( int i = 0; i < originalValue.size(); i++ ) {
receiver.indexedValue( "<iterable element>", i, originalValue.get( i ) );
}
}
}
The right callback methods (indexedValue()
, keyedValue()
etc.) must be called in order to allow for proper construction of the property path as per the rules laid out in the BV 1.1 spec.
If a non-null value is passed for nodeName
, a path node of type CONTAINER_ELEMENT
will be appended to the property path.
That’s desirable for collection types for instance. If null is passed, no node will be appended,
resulting in the same path as if the constraint had been given on the element itself instead of a type parameter.
That’s desirable for pure "wrapper types" such as Optional
.
@ExtractedValue
The @ExtractedValue
annotation is used to denote the element extracted by a given value extractor:
Unresolved directive in BVAL-508-appendix.asciidoc - include::{validation-api-source-dir}javax/validation/valueextraction/ExtractedValue.java[lines=7..8;15..-1]
The @ExtractedValue
annotation must be specified exactly once for a value extractor type.
Thus the following extractor definition is illegal as it specifies @ExtractedValue
several times:
public class DoubleExtractor implements ValueExtractor<Multimap<@ExtractedValue ?, @ExtractedValue ?>> { ... }
When defined for a generic type, only wildcard type arguments may be annotated with @ExtractedValue
.
Thus the following extractor definition is illegal:
public class StringListValueExtractor implements ValueExtractor<List<@ExtractedValue String>> { ... }
This implies that there may not be more than one extractor for a given generic type.
I.e. there can be an extractor for |
In case an illegal value extractor definition is detected, a ValueExtractorDefinitionException
is raised.
If a value extractor returns the entire extracted type itself
(as it is the case for the built-in extractor for handling any non-collection object associations),
the @ExtractedValue
annotation is to be given on the extracted type itself:
class ObjectValueExtractor implements ValueExtractor<@ExtractedValue Object> {
@Override
public void extractValues(Object originalValue, ValueReceiver receiver) {
receiver.value( null, originalValue );
}
}
If a value extractor returns the elements of an array type,
the @ExtractedValue
annotation is to be given for the array type as follows:
class IntArrayValueExtractor implements ValueExtractor<int @ExtractedValue[]> {
@Override
public void extractValues(int[] originalValue, ValueReceiver receiver) {
for ( int i = 0; i < originalValue.length; i++ ) {
receiver.indexedValue( "<iterable element>", i, originalValue[i] );
}
}
}
Motivation for callback-style API
Instead of returning the extracted values from the method call, implementations of |
Extensions to Node
There is a new javax.validation.ElementKind
, CONTAINER_ELEMENT
.
There is also a new Node
sub-type, ContainerElementNode
:
interface ContainerElementNode extends Node {
/**
* @return the type of the container the node is placed in, if contained in a container type such as
* {@code Optional}, {@code List} or an array, {@code null} otherwise
*/
Class<?> getContainerClass();
/**
* @return the index of the type parameter affected by the violated constraint in the container class
*/
Integer getTypeArgumentIndex();
}
getContainerClass()
and getTypeArgumentIndex()
also need to be added to BeanNode
and PropertyNode
and will return the affected type parameter when violating a class-level or property constraint in the course of cascaded validation of a generic type, such as List
or Map
.
Default extractors
Compatible implementations provide extractors for the following types out of the box. They must invoke the right callback methods in order to ensure path nodes in the described form are appended:
-
Arrays of objects and all primitive data types
-
@Valid
can be given for the array itself or for its component type. Both will cause the validation of all the array elements -
If a constraint given for an array’s component type is validated, a node with the following properties will be added to the path:
-
name: "<iterable element>"
-
kind:
CONTAINER_ELEMENT
-
isInIterable:
false
-
index: the element’s index
-
key:
null
-
containerClass: in the case of an array of
Object
s,Object[]
, in the case of an array of primitives, the type of the array (e.g.int[]
) -
typeArgumentIndex:
null
-
-
-
java.util.Iterable
- excluding the case ofjava.util.List
described below-
When
@Valid
is given on the iterable element itself, the element and all its entries will be validated; this is to grant backwards compatibility with BV 1.1 -
When
@Valid
is given on the type parameter of an iterable element, all the entries will be validated. -
When validating a type argument constraint for
Iterable
, a node with the following properties will be added to the path:-
name: "<iterable element>"
-
kind:
CONTAINER_ELEMENT
-
isInIterable:
true
-
index: the element’s index if the iterable is of type
List
or a subtype thereof;null
otherwise -
key:
null
-
containerClass:
Iterable
-
typeArgumentIndex:
0
-
-
-
java.util.List
-
When
@Valid
is given on the iterable element itself, the element and all its entries will be validated; this is to grant backwards compatibility with BV 1.1 -
When
@Valid
is given on the type parameter of an iterable element, all the entries will be validated. -
When validating a type argument constraint for
List
, a node with the following properties will be added to the path:-
name: "<iterable element>"
-
kind:
CONTAINER_ELEMENT
-
isInIterable:
true
-
index: the element’s index
-
key:
null
-
containerClass:
List
-
typeArgumentIndex:
0
-
-
-
java.util.Map
-
When
@Valid
is given on the map element itself, the element and all its values will be validated; this is to grant backwards compatibility with BV 1.1 -
When
@Valid
is given on the key type parameter, the map keys will be validated -
When
@Valid
is given on the value type parameter, the map values will be validated -
When validating a constraint on the key type argument of
Map
, a node with the following properties will be added to the path:-
name: "<map key>"
-
kind:
CONTAINER_ELEMENT
-
isInIterable:
true
-
index:
null
-
key: key
-
containerClass:
Map
-
typeArgumentIndex:
0
-
-
When validating a constraint on the value type argument of
Map
, a node with the following properties will be added to the path:-
name: "<map value>"
-
kind:
CONTAINER_ELEMENT
-
isInIterable:
true
-
index:
null
-
key: key
-
containerClass:
Map
-
typeArgumentIndex:
1
-
-
-
java.util.Optional
-
No node will be appended to the path when validating type argument constraints on
Optional
-
-
javafx.beans.observable.ObservableValue
-
No node will be appended to the path when validating type argument constraints on
ObservableValue
-
Constraints given on an element of type
ObservableValue
apply to the wrapped value by default (see Applying element-level constraints to wrapped elements).
-
-
java.lang.Object
-
When
@Valid
is given for an element, the element will be validated
-
Plugging in custom extractors
Additional value extractors can be plugged in when bootstrapping a Validator
or ValidatorFactory
, amending and/or overriding the set of built-in value extractors.
The following ways to do so exist, in descending order of precedence:
-
Invoking the method
ValidatorContext#addValueExtractor(ValueExtractor<?>)
(to apply it for a singleValidator
) -
Invoking the method
Configuration#addValueExtractor(ValueExtractor<?>)
(to apply it at the validation factory level) -
Specifying the fully-qualified class name of one or several extractors in
META-INF/validation.xml
:<value-extractors> <value-extractor>com.example.MyExtractor</value-extractor> </value-extractors>
-
Placing a file named META-INF/services/javax.validation.valueextraction.ValueExtractor on the classpath, with the fully-qualified name(s) of one or more extractor implementations as its contents (i.e. employing the Java service loader mechanism)
A value extractor for a given type and extracted type parameter specified at a higher level overrides any other extractors for the same type and extracted type given at lower levels.
If e.g. a value extractor defined as class MyListValueExtractor implements ValueExtractor<List<@ExtractedValue ?>> { … }
is given via ValidatorContext#addValueExtractor(ValueExtractor<?>)
,
it will take precedence over any other value extractor implementing List<@ExtractedValue ?>
given via Configuration#addValueExtractor(ValueExtractor<?>)
,
META-INF/validation.xml or the service loader mechanism as well as the built-in extractor for List
elements.
Applying element-level constraints to wrapped elements
Sometimes there is no type parameter to put a constraint to, but still constraints should be applied to the wrapped value instead of the annotated element
(a field, property getter, method return value or executable parameter).
JavaFX’s property hierarchy falls into that category, as it defines specific Property
sub-types which are not generic.
As an example, consider a generic type Wrapper<T>
and a non-generic sub-type StringWrapper
which binds the type parameter <T>
to String
:
@Email StringWrapper email;
Two defined sub-types of javax.validation.Payload
can be used to control the target of validation in such cases via the constraint’s payload()
attribute:
Payload
types for unwrapping controlUnresolved directive in BVAL-508-appendix.asciidoc - include::{validation-api-source-dir}javax/validation/valueextraction/Unwrapping.java[lines=7..8;11..-1]
@Email(payload = Unwrapping.Unwrap.class) StringWrapper email;
Value extractor definitions can be marked with the @UnwrapByDefault
annotation so that constraints are automatically applied to the wrapped value if a constraint is found for an element handled by that extractor:
@UnwrapByDefault
annotationUnresolved directive in BVAL-508-appendix.asciidoc - include::{validation-api-source-dir}javax/validation/valueextraction/UnwrapByDefault.java[lines=7..8;16..-1]
@UnwrapByDefault
class WrapperExtractor implements ValueExtractor<Wrapper<@ExtractedValue ?>> {
@Override
public void extractValues(Wrapper originalValue, ValueReceiver receiver) {
// ...
}
}
If this extractor was identified as the single most-specific extractor for StringWrapper
(see Retrieval of extractors), the @Email
constraint above would automatically be applied to the wrapped string value.
In rare cases it may happen that a constraint should be applied to the wrapped value although an extractor exists.
In this case the Unwrapping.Skip
payload type can be specified for that constraint to prevent the unwrapping:
@NotNull(payload = Unwrapping.Skip.class) StringWrapper email;
For the sake of readability, when applying constraints to the elements of a generic container type,
it is strongly recommended to put the constraints to the type argument instead of the element itself in conjunction with
over
|
Retrieval of extractors
When detecting a type argument constraint or cascade, the applicable extractor is determined as follows:
-
Select all those value extractors which handle a type parameter that maps to the type argument annotated with the constraint or
@Valid
annotation; Example:-
Given
List<@Email String> emails
and considering the default extractors listed above, only the extractors forList
andIterable
are selected. The former handles the type parameterT
of typeList
, which directly maps to the type argument annotated with@Email
. The latter handles the type parameterE
of typeIterable
which (indirectly) maps to the annotated type argument (asList
extendsIterable
and binds its type parameterT
toE
fromIterable
). Other extractors such as the ones forK
andV
ofMap
are dismissed, as they handle type parameters not mapping to the annotated type argument -
Given
interface ConfusingMap<K, V> extends Map<V, K> {}
ConfusingMap<@Email String, String> map;
And considering the default extractors listed above, only the extractor for the type parameter
V
ofMap
will be selected. This is because the@Email
constraint is given for the type argument representing type parameterK
ofConfusingMap
which maps to type parameterV
ofMap
.
-
-
From the remaining candidate value extractors choose the one which is most specific to the container type declaring the annoted type argument. An extractor A is more specific than another extractor B if A extracts a subtype of the type extracted by B. Example:
-
When obtaining the extractor for type parameter constraint validation, the declared type of the validated element is considered. This is to be consistent with constraint validator resolution, which is based on the static type of elements, not the runtime type.
-
When obtaining the extractor for cascaded validation, the runtime type of the cascaded element is considered. This is to be consistent with the rules defined for property path construction which are based on the runtime type.
-
From the two extractors above, the one for
List
is chosen asList
is a subtype ofIterable
.
-
-
If there are several extractors which are equally specific (e.g. several extractors for
List
), anUnexpectedTypeException
is raised. TODO: apply rules similar to "ConstraintValidator resolution algorithm" and further clarify wording
When detecting a regular element-level constraint (i.e. non type argument constraint) the applicable value extractor, if any, is determined as follows:
-
If the constraint carries the
Unwrapping.Skip
payload, don’t apply any value extractor -
Determine the set of uniquely mapping type parameters declared by the types in the element type’s type hierarchy; Examples:
-
element of type
java.lang.String
: () (empty set) -
element of type
java.lang.Iterable
: (T) -
element of type
java.lang.Map
: (K, V) -
element of type
java.util.Collection
: (E) (as the type parameterE
ofCollection
maps toT
ofIterable
, only the type parameter of the subtype is considered) -
interface A<T> {}
,interface B<U> {}
,class C implements A<String>, B<Integer> {}
; element of typeC
: (T, U) (two non mapping type parameters)
-
-
If the constraint carries the
Unwrapping.Unwrap
payload:-
If no type parameter or more than one type parameter was found in step 2, raise a
ConstraintDeclarationException
-
Choose the most specific extractor matching the single type parameter found in step 2
-
If there are several extractors which are equally specific, a
ConstraintDeclarationException
is raised. -
If there is exactly one remaining extractor, apply this extractor
-
Otherwise, a
ConstraintDeclarationException
is raised.
-
-
If the constraint neither carries the
Unwrapping.Unwrap
nor theUnwrapping.Skip
payload:-
If no type parameter or more than one type parameter was found in step 2, don’t apply any value extractor
-
Choose the most specific extractor matching the single type parameter found in step 2
-
If there are multiple such extractors, a
ConstraintDeclarationException
is raised. -
If there is exactly one remaining extractor and it is marked with
UnwrapByDefault
, apply this extractor -
Otherwise, no extractor is applied
-
Implementation note
As extractor retrieval for type parameter constraints is done using the static type of constrained elements, the retrieval can be done once at initialization time and then be cached. This is not possible for retrieval of extractors for cascaded validation. |
Examples
-
Applying a constraint to the value wrapped by a container type:
Property<@Min(1) Integer> value;
Note that
@Valid
is not required; the@Min
constraint will be validated when thevalue
property is subject to validation. -
Applying constraints to each value in a collection type:
List<@NotNull @Email String> emails;
-
Cascaded validation of the values in a collection type:
List<@Valid Order> orders;
This will validate the constraints on each
Order
element in the list. -
The legacy style for cascaded validation is supported as well:
@Valid List<Order> orders;
This would also validate any constraints on a custom list type (e.g.
MyList#getId()
). TODO: we never clarified that in 1.1. Should it be made explicit? -
Map validation with type argument constraints and cascading:
@Valid Map<@RegExp(...) String, @RetailOrder Order> orders;
This would validate the map’s keys against
@RegExp
, the map’s values against@RetailOrder
and apply cascaded validation of the map values (as well as the map object itself). -
When selecting extractors, type parameters must be thoroughly traced in the hierarchy. Consider this case where the order of the type parameters of
Map
is swapped in a sub-type:public class CrazyMap<K, V> implements Map<V, K> { ... }
public class Example { private CrazyMap<@RegExp(...) String, @Min(0) Long> crazyMap = ...; }
Assuming there is no dedicated extractor for
CrazyMap
but only the default ones forK
andV
ofMap
, extraction for@RegExp
must happen via the default map value extractor and extraction for@Min
via the default map key extractor.A type parameter in a sub-type may also map to several type parameters in a super-type:
interface NumericMap<T extends Number> extends Map<T, T> {}
private NumericMap<@Min(1) Integer> integerMap;
The
@Min
constraint is to be applied to the map’s keys and values as the annotated type parameter maps toK
andV
ofMap
. -
type argument constraints can be applied to the elements of
Object
arrays and arrays of any primitive type:String @Email[] emails;
int @Min(1) [] positiveNumbers;
-
The extractor for cascaded validation is determined based on an element’s runtime type:
Collection<@Valid Order> orders = new ArrayList<>();
Here the most-specific extractor for the runtime type
ArrayList
must be applied, causing the property nodes of violations to have an index set (Node#getIndex()
). -
The container value passed to a value extractor is retrieved from the element that has the type argument carrying the constraint or
@Valid
annotation:private Map<String, @Valid @RetailOrder Order> ordersByName; public Map<@NotNull String, Order> getOrdersByName() { return ordersByName; }
When validating the @NotNull
constraint, the map as returned by the getter will be passed to the map key extractor in order to obtain the map keys.
When validating the @RetailOrder
constraint and performing cascaded validation,
the map as obtained directly from the field will be passed to the map value extractor in order to obtain the map values.
-
Custom extractor for a
Tuple
type:public interface Tuple<T1, T2> { T1 getFirst(); T2 getSecond(); }
public class TupleFirstExtractor implements ValueExtractor<Tuple<@ExtractedValue ?, ?>> { @Override public void extractValues(Tuple<?, ?> originalValue, ValueReceiver receiver) { receiver.value( "<first>", originalValue.getFirst() ); } }
public class TupleSecondExtractor implements ValueExtractor<Tuple<?, @ExtractedValue ?>> { @Override public void extractValues(Tuple<?, ?> originalValue, ValueReceiver receiver) { receiver.value( "<second>", originalValue.getSecond() ); } }
private Tuple<@NotNull @Email String, @NotNull @Min(1) Integer> tuple;
Examples for extractor retrieval
-
The most specific extractor matching the constrained type argument is chosen:
private List<@Email String> emails;
Based on the algorithm described above and considering the mandated default extractors, only the extractor for
List
andIterable
are candidate extractors (all other extractors are defined for a type parameter not mapping toT
ofList
). The extractor forList
will be applied as it’s more specific than the extractor forIterable
(List
is a subtype ofIterable
). -
Constraints targeting wrapped values can be given on the wrapping element. Let there be these definitions:
class StringWrapper { String wrapped; };
@UnwrapByDefault class StringWrapperExtractor implements ValueExtractor<@ExtractedValue StringWrapper> { ... } };
private @Email StringWrapper email;
The
@Email
constraint will be applied to the wrapped string and can be validated as the extractor defines that element-level constraints should be applied to the wrapped value.If the extractor were not decorated with
@UnwrapByDefault
an exception would be raised as there is no validator for@Email
onStringWrapper
.Unwrapping could be mandated explicitly in this case:
@Email(payload = Unwrapping.Unwrap.class) private StringWrapper email;
Invalid examples
-
No most specific extractor can be found unambiguously:
public interface CachedValue<V> { V getCachedValue(); }
public interface RealValue<V> { V getRealValue(); }
public class CachableValue<V> implements CachedValue<V>, RealValue<V> { ... }
public class CachedValueExtractor implements ValueExtractor<CachedValue<@ExtractedValue ?>> { ... }
public class RealValueExtractor implements ValueExtractor<RealValue<@ExtractedValue ?>> { ... }
private CachableValue<@Min(1) Integer> foo;
Validation of
foo
will fail, as none of the two matching extractors is more specific than the other one. An extractor forCachableValue
must be added, resolving the ambiguity. -
Element-level constraints cannot be applied if there is no type parameter at all or multiple non-mapping type parameters in the annotated element’s type hierarchy. Thus an exception will be raised in the following cases:
// no type parameter @Email(payload = Unwrapping.Unwrap.class) private String email;
// multiple type parameters @Email(payload = Unwrapping.Unwrap.class) private Map<String, String> emails;
Misc.
-
Regarding group sequences and default group sequences, the same rules apply for type argument constraints as they apply for regular element-level constraints.
-
For the conversion of validation groups the same rules apply no matter whether
@Valid
is given for a regular element or for a type argument. I.e. the following group conversion declaration is valid:
private List<@Valid @ConvertGroup(from=Default.class, to=Other.class) Order> orders;
Open questions
-
Should nested containers be supported:
List<Map<String, @NotNull String>> addressesByType
? OrOptional<List<@Email>> optionalEmails
; The latter seems very reasonable. -
ConstraintsApplyTo
only allows one behavior per annotated element. Should it be per constraint? E.g. for@NotNull @Email StringProperty email
it may be desirable to apply@NotNull
to the wrapper but@Email
to the wrapped value. That’s not possible currently.For the first draft we settled for using dedicated
Payload
types to control the unwrapping behavior. While not ideal, this provides the required level of control. Discussed alternatives include a new constraint memberboolean validateWrappedValue()
, but this requires to update every existing constraint and adds a lot of "weight" for a rather small corner case. Any approach using a separate annotation fails as there is no way to clearly identify the targeted constraint annotation, as annotations cannot obtained from an element in a guaranteed order. -
Should
ConstraintsApplyTo
also be used for tagging extractors triggering "auto-extraction". Maybe a separate annotation would be less confusing, e.g.@AutoExtract
or so?This is done by
@UnwrapByDefault
now which solely exists for this purpose. -
Should a path node be added for type argument constraints of
Optional
and similar types?This proposal suggests to not do it, but Emmanuel is not convinced of this.
-
Should value extractors be discoverable via the service loader mechanism (i.e. by means of
META-INF/services/javax.validation.valueextraction.ValueExtractor
files)Pro: It’d allow 3rd party libs such as Google Guava to provide custom extractors for their container types and have them automatically be applied without any effort for the user.
Cons: Need a way to disable or override some extractors with others. Which might make it a nogo.
Added service loader mechanism as source of value extractors. Specified order of precedence.
-
What to return from
PropertyDescriptor#getElementClass()
if there is a field of typeFoo
but a getter of typeOptional<Foo>
. So far, BV assumed the types of field and getter to be the same and exposed a single property descriptor (which btw. also may fall apart as of BV 1.1 when the field is of a sub-type of the getter’s type). What to return here? -
Should the presence of type argument constraints alone trigger cascaded validation?
E.g. consider the case of
Tuple
above:Tuple<@Min(1) Integer, @Email String> tuple;
Here it may be nice to validate e.g.
@NotNull
constraints given within theTuple
class itself when validating the type argument constraints. With the current proposal their validation requires a separate@Valid
on the element. Personally I think that’s better (more consistent).We agreed to require
@Valid
for the sake of consistency. -
For an element with a type argument, should it be allowed to specify constraints on the element (and use the
Unwrapping.Unwrap
payload) or should it be disallowed?@Email(payload = Unwrapping.Unwrap.class) Optional<String> email;
-
Should we allow extractors to be defined for specific parameterized types, e.g.:
public class ListOfIntegerExtractor implements ValueExtractor<List<@ExtractedValue Integer>> { ... } public class ListOfStringExtractor implements ValueExtractor<List<@ExtractedValue String>> { ... }
Currently, only one extractor (for type
List<?>
is allowed).I can’t see a compelling use case for this (when would extractor behavior differ between different parameterizations of the same generic type) and am leaning towards only supporting the wildcard parameterization (
implements ValueExtractor<List<@ExtractedValue ?>>
).We agreed on only allowing one extractor per type, to be given using the wildcard parameterization, as we cannot see a use case for having multiple extractors right now. But if needed, this limitation can be lifted in a future revision.
-
Can we find another name than "type argument constraints"? While that suits for the most cases, it doesn’t when applying constraints to the component type of an array:
String @NotBlank [] names
.I think "type use" is the correct one in Java terminology. But would anyone get what a "type use constraint" is?.
-
Vet the API by exploring advanced use cases, e.g. Guava’s Table, Graph, ValueGraph and Network types.
Example for
Table
:class TableValueExtractor implements ValueExtractor<Table<?, ?, @ExtractedValue ?>> { @Override public void extractValues(Table<?, ?, ?> originalValue, ValueExtractor.ValueReceiver receiver) { for ( Cell<?, ?, ?> cell : originalValue.cellSet() ) { receiver.keyedValue( "<table cell>", new CellKey( cell.getRowKey(), cell.getColumnKey() ), cell.getValue() ); } } }
public static class CellKey { private final Object rowKey; private final Object columnKey; public CellKey(Object rowKey, Object columnKey) { this.rowKey = rowKey; this.columnKey = columnKey; } // equals(), hashCode() ... }
When having an invalid table cell in the following and validating it:
public class Customer { Table<Year, String, @Min(1) Integer> revenuePerYearAndCategory = HashBasedTable.create(); }
Then this will be the result:
ConstraintViolation<Customer> violation = ...; Iterator<Node> path = violation.getPropertyPath().iterator(); Node node = path.next(); assertThat( node.getName() ).isEqualTo( "revenuePerYearAndCategory" ); assertThat( node.getKind() ).isEqualTo( ElementKind.PROPERTY ); assertThat( node.getKey() ).isNull(); assertThat( node.getIndex() ).isNull(); node = path.next(); assertThat( node.getName() ).isEqualTo( "<table cell>" ); assertThat( node.getKind() ).isEqualTo( ElementKind.CONTAINER_ELEMENT ); assertThat( node.getKey() ).isEqualTo( new CellKey(Year.of( 2015 ), "cds") ); assertThat( node.getIndex() ).isNull(); assertThat( path.hasNext() ).isFalse();
-
In the original proposal it was foreseen that
@ExtractedValue
could refer to type-parameters from super-types. Is that still needed? -
During cascaded validation of an element with several type arguments, it’s currently not possible to tell from the resulting constraint violation and its node path which type argument was cascaded. Example:
Map<@Valid OrderType, @Valid Order> ordersByType;
If there was constraint violation on an
OrderType
property and one on anOrder
property, one couldn’t tell from the resulting paths and their nodes which is which.One way out could be to add
TypeVariable<?> Node#getTypeParameter()
.This would return the type parameter handled by the extractor used for obtaining the cascaded value.
Node#getTypeParameter()
would also return the type parameter for type argument constraints. Note it must begetTypeParameter()
(notgetTypeArgument()
) because one annotated type argument at the constrained/cascaded element could represent multiple type parameters (see example "A type parameter in a sub-type may also map to several type parameters in a super-type" above). Assuming a constraint violation on propertydescription
of classOrderType
we’d get:ConstraintViolation<Customer> violation = ...; Iterator<Node> path = violation.getPropertyPath().iterator(); Node node = path.next(); assertThat( node.getName() ).isEqualTo( "ordersByType" ); assertThat( node.getKind() ).isEqualTo( ElementKind.PROPERTY ); assertThat( node.getKey() ).isNull(); assertThat( node.getIndex() ).isNull(); assertThat( node.getTypeParameter() ).isNull(); node = path.next(); assertThat( node.getName() ).isEqualTo( "description" ); assertThat( node.getKind() ).isEqualTo( ElementKind.PROPERTY ); assertThat( node.getKey() ).isEqualTo( new OrderType( "RETAIL" ) ); assertThat( node.getIndex() ).isNull(); assertThat( node.getTypeParameter().getName() ).isEqualTo( "K" ); assertThat( path.hasNext() ).isFalse();
getTypeParameter()
would also return the type parameter in case of type argument constraints:Map<OrderType, @Min(1) Integer> orderQuantitiesByType;
ConstraintViolation<Customer> violation = ...; Iterator<Node> path = violation.getPropertyPath().iterator(); Node node = path.next(); assertThat( node.getName() ).isEqualTo( "orderQuantitiesByType" ); assertThat( node.getKind() ).isEqualTo( ElementKind.PROPERTY ); assertThat( node.getKey() ).isNull(); assertThat( node.getIndex() ).isNull(); assertThat( node.getTypeParameter() ).isNull(); node = path.next(); assertThat( node.getName() ).isEqualTo( "<map value>" ); assertThat( node.getKind() ).isEqualTo( ElementKind.CONTAINER_ELEMENT ); assertThat( node.getKey() ).isEqualTo( new OrderType( "RETAIL" ) ); assertThat( node.getIndex() ).isNull(); assertThat( node.getTypeParameter().getName() ).isEqualTo( "V" ); assertThat( path.hasNext() ).isFalse();
-
Revisit the names of nodes of kind
CONTAINER_ELEMENT
; instead of<iterable element>
,<map value>
etc. should it be the names of the type parameters in question, i.e.<E>
,<V>
etc.?