The constraintValidatorContext fluent API for building errors. Please
review.
Begin forwarded message:
From: Emmanuel Bernard <emmanuel.bernard(a)jboss.com>
Date: June 18, 2009 16:55:06 EDT
To: jsr-303-eg(a)jcp.org
Subject: Re: [jsr-303-eg] Path, string or object model
Here is the solution for ConstraintValidatorContext.
I've designed a fluent API to create error reports and add their
message and potentially their sub nodes and the appropriate
contextual information.
Here is a usage example
public boolean isValid(String value, ConstraintValidatorContext
context) {
//default path
context.buildErrorWithMessage( "this detail is wrong" ).addError();
//default path + "street"
context.buildErrorWithMessage( "this detail is wrong" )
.inSubNode( "street" )
.addError();
//default path + "addresses["home"].country.name
context.buildErrorWithMessage( "this detail is wrong" )
.inSubNode( "addresses" )
.inSubNode( "country" )
.inIterable().atKey( "home" )
.inSubNode( "name" )
.addError();
return false;
}
I've used a nested interface model to keep things clean. Please
review and comment.
/**
* Provide contextual data and operation when applying a given
constraint validator
*
* @author Emmanuel Bernard
*/
public interface ConstraintValidatorContext {
/**
* Disable the default error message and default
ConstraintViolation object generation.
* Useful to set a different error message or generate a
ConstraintViolation based on
* a different property
*/
void disableDefaultError();
/**
* @return the current uninterpolated default message
*/
String getDefaultErrorMessage();
/**
* Return an error builder building an error allowing to optionally
associate
* the error to a sub path.
* The error message will be interpolated.
* <p/>
* To create the error, one must call either one of
* the #addError method available in one of the
* interfaces of the fluent API.
* If another method is called after #addError() on
* ErrorBuilder or any of its associated nested interfaces
* an IllegalStateException is raised.
* <p/>
* If <code>isValid<code> returns <code>false</code>, a
<code>ConstraintViolation</code> object will be built
* per error including the default one unless {@link
#disableDefaultError()} has been called.
* <p/>
* <code>ConstraintViolation</code> objects generated from such a
call
* contain the same contextual information (root bean, path and so
on) unless
* the path has been overriden.
* <p/>
* To create a different error, a new error builder has to be
retrieved from
* ConstraintValidatorContext
*
* Here are a few usage examples:
* <pre>//create new error with the default path the constraint
* //is located on
* context.buildErrorWithMessage( "way too long" )
* .addError();
*
* //create new error in the "street" subnode of the default
* //path the constraint is located on
* context.buildErrorWithMessage( "way too long" )
* .inSubNode( "street" )
* .addError();
*
* //create new error in the "addresses["home"].city.name
* //subnode of the default path the constraint is located on
* context.buildErrorWithMessage( "this detail is wrong" )
* .inSubNode( "addresses" )
* .inSubNode( "country" )
* .inIterable().atKey( "home" )
* .inSubNode( "name" )
* .addError();
* </pre>
*
* @param message new uninterpolated error message.
*/
ErrorBuilder buildErrorWithMessage(String message);
/**
* Error builder allowing to optionally associate
* the error to a sub path.
*
* To create the error, one must call either one of
* the #addError method available in one of the
* interfaces of the fluent API.
* If another method is called after #addError() on
* ErrorBuilder or any of its associated objects
* an IllegalStateException is raised.
*
*/
interface ErrorBuilder {
/**
* Add a subNode to the path the error will be associated to
*
* @param name property
* @return a builder representing the first level node
*/
NodeBuilder inSubNode(String name);
/**
* Add the new error report to be generated if the
* constraint validator mark the value as invalid.
* Methods of this ErrorBuilder instance and its nested
* objects returns IllegalStateException from now on.
*
* @return ConstraintValidatorContext instance the ErrorBuilder
comes from
*/
ConstraintValidatorContext addError();
/**
* Represent asubnode whose context is known
* (ie index, key and isInIterable)
*/
interface NodeBuilder {
/**
* Add a subNode to the path the error will be associated to
*
* @param name property
* @return a builder representing this node
*/
InIterableNodeBuilder inSubNode(String name);
/**
* Add the new error report to be generated if the
* constraint validator mark the value as invalid.
* Methods of the ErrorBuilder instance this object comes
* from and the error builder nested
* objects returns IllegalStateException from now on.
*
* @return ConstraintValidatorContext instance the ErrorBuilder
comes from
*/
ConstraintValidatorContext addError();
}
/**
* Represent a subnode whose context is
* configurable (ie index, key and isInIterable)
*/
interface InIterableNodeBuilder {
/**
* Mark the node as being in an Iterable or a Map
* @return a builder representing iterable details
*/
InIterablePropertiesBuilder inIterable();
/**
* Add a subNode to the path the error will be associated to
*
* @param name property
* @return a builder representing this node
*/
InIterableNodeBuilder inSubNode(String name);
/**
* Add the new error report to be generated if the
* constraint validator mark the value as invalid.
* Methods of the ErrorBuilder instance this object comes
* from and the error builder nested
* objects returns IllegalStateException from now on.
*
* @return ConstraintValidatorContext instance the ErrorBuilder
comes from
*/
ConstraintValidatorContext addError();
}
/**
* Represent choices for a node which is
* in an Iterator or Map.
* If the iterator is an indexed collection or a map,
* the index or the key should be set.
*/
interface InIterablePropertiesBuilder {
/**
* Define the key the object is into the Map
*
* @param key map key
* @return a builder representing the current node
*/
NodeBuilder atKey(Object key);
/**
* Define the index the object is into the List or array
*
* @param index index
* @return a builder representing the current node
*/
NodeBuilder atIndex(Integer index);
/**
* Add a subNode to the path the error will be associated to
*
* @param name property
* @return a builder representing this node
*/
InIterableNodeBuilder inSubNode(String name);
/**
* Add the new error report to be generated if the
* constraint validator mark the value as invalid.
* Methods of the ErrorBuilder instance this object comes
* from and the error builder nested
* objects returns IllegalStateException from now on.
*
* @return ConstraintValidatorContext instance the ErrorBuilder
comes from
*/
ConstraintValidatorContext addError();
}
}
}
On Jun 17, 2009, at 14:41, Emmanuel Bernard wrote:
> Here is the solution I cooked. I think it's acceptable but add some
> complexity in the ConstraintValidatorContext API (see other email).
> Please review (pay special attention to the examples).
>
> A Path represents the path and is the one accepted by the path
> consuming APIs. A Path is an Iterable of Nodes.
>
> /**
> * Represent a navigation path from an object to another.
> * Each path element is represented by a Node.
> *
> * The path corresponds to the succession of nodes
> * in the order they are retured by the Iterator
> *
> * @author Emmanuel Bernard
> */
> public interface Path extends Iterable<Node> {
> }
>
>
> A node represent a path element.
>
> /**
> * Represents an element of a navigation path
> *
> * @author Emmanuel Bernard
> */
> public interface Node {
> /**
> * Property name the node represents
> * or null if the leaf node and representing an entity
> * (in particular the node representing the root object
> * has its name null)
> */
> String getName();
>
> /**
> * True if the node represents an object contained in an Iterable
> * or in a Map.
> */
> boolean isInIterable();
>
> /**
> * The index the node is placed in if contained
> * in an array or List. Null otherwise.
> */
> Integer getIndex();
>
> /**
> * The key the node is placed in if contained
> * in a Map. Null otherwise.
> */
> Object getKey();
> }
>
> A few interesting points:
> - the index / key is hosted by the node after the collection node
>
> Here are a few examples and their Node equivalent
>
> ""
> 0: Node(name:null, isInIterable:false, index:null, key:null)
>
> "email"
> 0: Node(name:email, isInIterable:false, index:null, key:null)
>
> "addresses"
> 0: Node(name:addresses, isInIterable:false, index:null, key:null)
>
> "addresses["home"]" represent the bean level of an Address
object
> 0: Node(name:addresses, isInIterable:false, index:null, key:null)
> 1: Node(name:null, isInIterable:true, index:null, key:home)
>
> "addresses["home"].city"
> 0: Node(name:addresses, isInIterable:false, index:null, key:null)
> 1: Node(name:city, isInIterable:true, index:null, key:home)
>
> "billingAddresses[3].country.name"
> 0: Node(name:billingAddresses, isInIterable:false, index:null,
> key:null)
> 1: Node(name:country, isInIterable:true, index:3, key:null)
> 2: Node(name:name, isInIterable:false, index:null, key:null)
>
>
> ConstraintViolation renders a Path
>
> public interface ConstraintViolation<T> {
> Path getPropertyPath();
> }
>
> TraversableResolver accepts a path
>
> public interface TraversableResolver {
> boolean isReachable(Object traversableObject,
> String traversableProperty,
> Class<?> rootBeanType,
> Path pathToTraversableObject,
> ElementType elementType);
> ...
> }
>
> PS: should String traversableProperty be Node traversableProperty ?
>
>
> On May 27, 2009, at 12:19, Emmanuel Bernard wrote:
>
>> In several areas we do describe path:
>> - ConstraintViolation
>> - ConstraintValidatorContext (with addError(String, String) which
>> allows to concatenate substrings
>>
>> So far we use the notion of string to represent it
>> - address.name
>> - cards[3].color
>> - addresses["home"].city
>>
>> I have added the idea of using [] for simple Iterable objects (ie
>> non indexed, like a Set)
>> - accounts[].balance
>>
>> Anybody objects to that?
>>
>> Second point
>> Do we want to replace this String approach with a path object mode?
>>
>>
http://opensource.atlassian.com/projects/hibernate/browse/BVAL-143
>> ______
>> path are today strings with dot separating properties. But it
>> break when Set or Iterable are used.
>> We could replace that with
>> --- First strawman, must evolve --
>> class PathElement {
>> String getName();
>> PathElement getParentPath();
>> boolean isIterable();
>> boolean isIndexed();
>> Object getIndex();
>> //TODO int getIndex()?
>>
>> // not happy about that as it is only useful for
>> Constraintciolation
>> PathElement getChild();
>> }
>>
>> PathElement would be used for Constraintvuilation, maybe CVContext
>> etc
>>
>> can this be refactored using inheritance + generics to have an
>> IndexedPathElement only when it matters (probably no unfortunately)
>> ______
>>
>>
>> Pros:
>> - less string manipulation by the user and the
>> TraversableResolver implementation
>> - the map index no longer rely on "[" + toString() + "]" and
is
>> likely more easily handled
>>
>> Cons:
>> - ConstraintValidatorContext becomes more complex as it needs to
>> expose some kind of path element builder.
>> - we would like need to standardize some kind of String
>> serialization anyway
>> - I don't see Pros as huge advantages
>>
>> WDYT?
>
> _______________________________________________
> jsr-303-eg mailing list jsr-303-eg(a)jcp.org