Last active
February 22, 2016 04:19
-
-
Save phaas/94b37d579b6eff9d1d60 to your computer and use it in GitHub Desktop.
Bean Validation - Context-specific validation messages (https://forum.hibernate.org/viewtopic.php?f=9&t=1042960&start=0)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package validation; | |
import java.util.List; | |
import java.util.Set; | |
import java.util.stream.Collectors; | |
import javax.validation.ConstraintViolation; | |
import javax.validation.Valid; | |
import javax.validation.Validation; | |
import javax.validation.Validator; | |
import javax.validation.constraints.NotNull; | |
import javax.validation.constraints.Size; | |
import org.hamcrest.Matchers; | |
import org.hibernate.validator.constraints.NotEmpty; | |
import org.junit.Assert; | |
import org.junit.Test; | |
import com.google.common.collect.ImmutableList; | |
/** | |
* Basic validation example, using only available functionality. | |
*/ | |
public class ValidationExample { | |
public static class Actor { | |
@NotEmpty(message = "First Name is required") | |
public final String firstName; | |
@NotEmpty(message = "Last Name is required") | |
public final String lastName; | |
public Actor(String firstName, String lastName) { | |
this.firstName = firstName; | |
this.lastName = lastName; | |
} | |
} | |
public static class Movie { | |
@Size(min = 1, message = "The cast must have at least one member") | |
@Valid | |
public final ImmutableList<Role> cast; | |
@NotEmpty(message = "Movie title is required") | |
public final String title; | |
public Movie(String title, ImmutableList<Role> cast) { | |
this.cast = cast; | |
this.title = title; | |
} | |
} | |
public static class Role { | |
@NotNull(message = "Actor is required") | |
@Valid | |
public final Actor actor; | |
@NotEmpty(message = "Character name is required") | |
public final String character; | |
public Role(Actor actor, String character) { | |
this.actor = actor; | |
this.character = character; | |
} | |
} | |
@Test | |
public void testEmptyMovie() { | |
Movie movie = new Movie("", ImmutableList.of()); | |
validate(movie, | |
"cast: The cast must have at least one member", | |
"title: Movie title is required"); | |
} | |
@Test | |
public void testMissingActor() { | |
Movie movie = new Movie("The Revenant", ImmutableList.of( | |
new Role(null, "Hugh Glass") | |
)); | |
validate(movie, "cast[0].actor: Actor is required"); | |
} | |
@Test | |
public void testMissingAndIncompleteActors() { | |
Movie movie = new Movie("The Revenant", ImmutableList.of( | |
new Role(null, "Hugh Glass"), | |
new Role(new Actor("Domhnall", null), "Captain Andrew Henry"), | |
new Role(new Actor("Tom", "Hardy"), null) | |
)); | |
validate(movie, | |
"cast[0].actor: Actor is required", | |
"cast[2].character: Character name is required", | |
"cast[1].actor.lastName: Last Name is required"); | |
} | |
private void validate(Movie movie, String... expectedMessages) { | |
final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); | |
Set<ConstraintViolation<Movie>> result = validator.validate(movie); | |
final List<String> messages = result.stream() | |
.map(cv -> cv.getPropertyPath().toString() + ": " + cv.getMessage()) | |
.collect(Collectors.toList()); | |
Assert.assertThat(messages, Matchers.containsInAnyOrder(expectedMessages)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package validation; | |
import java.util.List; | |
import java.util.Set; | |
import java.util.stream.Collectors; | |
import javax.validation.ConstraintViolation; | |
import javax.validation.Valid; | |
import javax.validation.Validation; | |
import javax.validation.Validator; | |
import javax.validation.constraints.NotNull; | |
import javax.validation.constraints.Size; | |
import javax.validation.groups.Default; | |
import org.hamcrest.Matchers; | |
import org.hibernate.validator.constraints.NotEmpty; | |
import org.junit.Assert; | |
import org.junit.Test; | |
import com.google.common.collect.ImmutableList; | |
/** | |
* Basic validation example, using only available functionality. | |
*/ | |
public class EnhancedValidationExample { | |
public interface ActorValidation { | |
} | |
public interface MovieValidation { | |
} | |
public interface MovieListValidation { | |
} | |
public static class Actor { | |
@NotEmpty.List({ | |
@NotEmpty(message = "First Name is required", groups = ActorValidation.class), | |
@NotEmpty(message = "First Name is required for role \"${character}\"", groups = MovieValidation.class), | |
@NotEmpty(message = "First Name is required for role \"${character}\" in movie \"${movieTitle}\"", groups = MovieListValidation.class) | |
}) | |
public final String firstName; | |
@NotEmpty.List({ | |
@NotEmpty(message = "Last Name is required", groups = ActorValidation.class), | |
@NotEmpty(message = "Last Name is required for role \"${character}\"", groups = MovieValidation.class), | |
@NotEmpty(message = "Last Name is required for role \"${character}\" in movie \"${movieTitle}\"", groups = MovieListValidation.class) | |
}) | |
public final String lastName; | |
public Actor(String firstName, String lastName) { | |
this.firstName = firstName; | |
this.lastName = lastName; | |
} | |
} | |
public static class Role { | |
@NotNull.List({ | |
@NotNull(message = "Actor is required for role \"${character}\"", groups = MovieValidation.class), | |
@NotNull(message = "Actor is required for role \"${character}\" in movie \"${movieTitle}\"", groups = MovieListValidation.class) | |
}) | |
@Valid | |
public final Actor actor; | |
@NotEmpty.List({ | |
@NotEmpty(message = "Character name is required for actor \"${actor}\"", groups = MovieValidation.class), | |
@NotEmpty(message = "Character name is required for actor \"${actor}\" in movie \"${movieTitle}\"", groups = MovieListValidation.class) | |
}) | |
public final String character; | |
public Role(Actor actor, String character) { | |
this.actor = actor; | |
this.character = character; | |
} | |
} | |
public static class Movie { | |
@Size.List({ | |
@Size(min = 1, message = "The cast must have at least one member", groups = MovieValidation.class), | |
@Size(min = 1, message = "The cast of \"${movieTitle}\" must have at least one member", groups = MovieListValidation.class), | |
}) | |
@Valid | |
public final ImmutableList<Role> cast; | |
@NotEmpty(message = "Movie title is required") | |
public final String title; | |
public Movie(String title, ImmutableList<Role> cast) { | |
this.cast = cast; | |
this.title = title; | |
} | |
} | |
public static class MovieList { | |
@Valid | |
@NotNull | |
public final ImmutableList<Movie> favorites; | |
public MovieList(ImmutableList<Movie> favorites) { | |
this.favorites = favorites; | |
} | |
} | |
@Test | |
public void testEmptyMovie() { | |
Movie movie = new Movie("", ImmutableList.of()); | |
validate(movie, | |
"cast: The cast must have at least one member", | |
"title: Movie title is required"); | |
} | |
@Test | |
public void testMissingActor() { | |
Movie movie = new Movie("The Revenant", ImmutableList.of( | |
new Role(null, "Hugh Glass") | |
)); | |
validate(movie, "cast[0].actor: Actor is required for role \"Hugh Glass\""); | |
} | |
@Test | |
public void testMissingAndIncompleteActors() { | |
Movie movie = new Movie("The Revenant", ImmutableList.of( | |
new Role(null, "Hugh Glass"), | |
new Role(new Actor("Domhnall", null), "Captain Andrew Henry"), | |
new Role(new Actor("Tom", "Hardy"), null) | |
)); | |
validate(movie, | |
"cast[0].actor: Actor is required for role \"Hugh Glass\"", | |
"cast[2].character: Character name is required for actor \"Tom Hardy\"", | |
"cast[1].actor.lastName: Last Name is required for character \"Captain Andrew Henry\""); | |
} | |
private void validate(Movie movie, String... expectedMessages) { | |
final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); | |
Set<ConstraintViolation<Movie>> result = validator.validate(movie, Default.class, MovieValidation.class); | |
final List<String> messages = result.stream() | |
.map(cv -> cv.getPropertyPath().toString() + ": " + cv.getMessage()) | |
.collect(Collectors.toList()); | |
messages.forEach(System.out::println); | |
Assert.assertThat(messages, Matchers.containsInAnyOrder(expectedMessages)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package validation; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.Iterator; | |
import java.util.List; | |
import javax.el.ELContext; | |
import javax.el.ExpressionFactory; | |
import javax.el.ValueExpression; | |
import javax.validation.ConstraintViolation; | |
import javax.validation.Path; | |
import org.hibernate.validator.internal.engine.messageinterpolation.el.SimpleELContext; | |
import org.hibernate.validator.internal.engine.path.NodeImpl; | |
import com.google.common.base.Joiner; | |
/** | |
* A means of rewriting ConstraintViolations to provide information about the | |
* path that is meaningful to the user. | |
* <p> | |
* Created by Patrick on 2/21/2016. | |
*/ | |
public class ConstraintViolationEnhancer { | |
/** | |
* An object that can provide a description of itself to assist a user in identifying this | |
* particular object. | |
*/ | |
public interface NodeDescription { | |
String getNodeDescription(); | |
} | |
private final boolean describeAllNodes; | |
private final ExpressionFactory expressionFactory; | |
private final Joiner joiner; | |
public ConstraintViolationEnhancer() { | |
this(false, ExpressionFactory.newInstance(), Joiner.on(" > ")); | |
} | |
/** | |
* @param describeAllNodes if true, all nodes on the path will be captured, using their toString form. If false, | |
* only nodes implementing {@link NodeDescription} will be printed. | |
* @param expressionFactory | |
* @param joiner | |
*/ | |
public ConstraintViolationEnhancer(boolean describeAllNodes, ExpressionFactory expressionFactory, Joiner joiner) { | |
this.describeAllNodes = describeAllNodes; | |
this.expressionFactory = expressionFactory; | |
this.joiner = joiner; | |
} | |
/** | |
* Describe this constraint violation by describing the objects on the path to the constraint violation, | |
* followed by the constraint violation message. | |
* | |
* @param violation | |
* @return | |
*/ | |
public String buildMessageWithPathInformation(ConstraintViolation<?> violation) { | |
final ValueExpression rootObject = expressionFactory.createValueExpression(violation.getRootBean(), Object.class); | |
SimpleELContext elContext = new SimpleELContext(); | |
elContext.setVariable("root", rootObject); | |
String pathStr = "root"; | |
final Iterator<Path.Node> iterator = violation.getPropertyPath().iterator(); | |
final List<String> descriptions = new ArrayList<>(); | |
while (iterator.hasNext()) { | |
final boolean isLast = !iterator.hasNext(); | |
if (isLast) { | |
// Assuming that the error message is specific to the field, | |
// we don't need to include the final node | |
continue; | |
} | |
final Path.Node node = iterator.next(); | |
pathStr += "." + getPathComponentAsElExpressionString(node); | |
addDescription(elContext, pathStr, descriptions); | |
} | |
descriptions.add(violation.getMessage()); | |
return joiner.join(descriptions); | |
} | |
/** | |
* Determine the description of the object specified by <code>path</code> in the | |
* <code>elContext</code> and, if available, add it to the list of <code>descriptions</code> | |
* | |
* @param elContext | |
* @param path | |
* @param descriptions | |
*/ | |
private void addDescription(ELContext elContext, String path, List<String> descriptions) { | |
final String expression = "${" + path + "}"; | |
ValueExpression expr = expressionFactory.createValueExpression(elContext, expression, Object.class); | |
final Object value = expr.getValue(elContext); | |
final String description = describeNode(value); | |
if (description != null && !description.isEmpty()) { | |
descriptions.add(description); | |
} | |
} | |
private String describeNode(Object value) { | |
if (value == null) { | |
return null; | |
} | |
if (value instanceof NodeDescription) { | |
return ((NodeDescription) value).getNodeDescription(); | |
} | |
if (describeAllNodes) { | |
if (value instanceof Collection) { | |
return null; | |
} else { | |
return String.valueOf(value); | |
} | |
} else { | |
return null; | |
} | |
} | |
private String getPathComponentAsElExpressionString(Path.Node node) { | |
// WARNING: Hibernate-specific implementation. Could copy NodeImpl#asString(..) implementation to | |
// make this more portable | |
return ((NodeImpl) node).asString(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package validation; | |
import java.util.List; | |
import java.util.Set; | |
import java.util.stream.Collectors; | |
import javax.validation.ConstraintViolation; | |
import javax.validation.Valid; | |
import javax.validation.Validation; | |
import javax.validation.Validator; | |
import javax.validation.constraints.NotNull; | |
import javax.validation.constraints.Size; | |
import org.hamcrest.Matchers; | |
import org.hibernate.validator.constraints.NotEmpty; | |
import org.junit.Assert; | |
import org.junit.Test; | |
import com.google.common.collect.ImmutableList; | |
/** | |
* Test rewriting of constraint violations to describe the objects leading to the failed constraint by | |
* traversing property path. | |
*/ | |
public class ConstraintViolationEnhancerTest { | |
public static class Actor implements ConstraintViolationEnhancer.NodeDescription { | |
@NotEmpty(message = "First Name is required") | |
private final String firstName; | |
@NotEmpty(message = "Last Name is required") | |
private final String lastName; | |
public Actor(String firstName, String lastName) { | |
this.firstName = firstName; | |
this.lastName = lastName; | |
} | |
public String getFirstName() { | |
return firstName; | |
} | |
public String getLastName() { | |
return lastName; | |
} | |
@Override | |
public String getNodeDescription() { | |
return null; // Skip | |
} | |
@Override | |
public String toString() { | |
return new StringBuilder() | |
.append(firstName == null ? "[blank]" : firstName) | |
.append(" ") | |
.append(lastName == null ? "[blank]" : lastName) | |
.toString(); | |
} | |
} | |
public static class Role implements ConstraintViolationEnhancer.NodeDescription { | |
@Valid | |
@NotNull(message = "Actor is required") | |
private final Actor actor; | |
@NotEmpty(message = "Character name is required") | |
private final String character; | |
public Role(Actor actor, String character) { | |
this.actor = actor; | |
this.character = character; | |
} | |
public Actor getActor() { | |
return actor; | |
} | |
public String getCharacter() { | |
return character; | |
} | |
@Override | |
public String getNodeDescription() { | |
return "Role \"" | |
+ (character == null ? "[missing character name]" : character) | |
+ "\", Actor \"" | |
+ (actor == null ? "[missing]" : actor.toString()) + "\""; | |
} | |
} | |
public static class Movie implements ConstraintViolationEnhancer.NodeDescription { | |
@Valid | |
@Size(min = 1, message = "The cast must have at least one member") | |
private final ImmutableList<Role> cast; | |
@NotEmpty(message = "Movie title is required") | |
private final String title; | |
public Movie(String title, ImmutableList<Role> cast) { | |
this.cast = cast; | |
this.title = title; | |
} | |
public ImmutableList<Role> getCast() { | |
return cast; | |
} | |
public String getTitle() { | |
return title; | |
} | |
@Override | |
public String getNodeDescription() { | |
return "Movie \"" + (title == null ? "[missing title]" : title) + "\""; | |
} | |
} | |
public static class MovieList { | |
@Valid | |
@NotNull | |
private final ImmutableList<Movie> movies; | |
public MovieList(ImmutableList<Movie> movies) { | |
this.movies = movies; | |
} | |
public ImmutableList<Movie> getMovies() { | |
return movies; | |
} | |
} | |
@Test | |
public void testEmptyMovie() { | |
Movie movie = new Movie("", ImmutableList.of()); | |
validate(movie, | |
"The cast must have at least one member", | |
"Movie title is required"); | |
} | |
@Test | |
public void testMissingActor() { | |
Movie movie = new Movie("The Revenant", ImmutableList.of( | |
new Role(null, "Hugh Glass") | |
)); | |
validate(movie, "Role \"Hugh Glass\", Actor \"[missing]\" > Actor is required"); | |
} | |
@Test | |
public void testMissingAndIncompleteActors() { | |
Movie movie = new Movie("The Revenant", ImmutableList.of( | |
new Role(null, "Hugh Glass"), | |
new Role(new Actor("Domhnall", null), "Captain Andrew Henry"), | |
new Role(new Actor("Tom", "Hardy"), null) | |
)); | |
validate(movie, | |
"Role \"Hugh Glass\", Actor \"[missing]\" > Actor is required", | |
"Role \"Captain Andrew Henry\", Actor \"Domhnall [blank]\" > Last Name is required", | |
"Role \"[missing character name]\", Actor \"Tom Hardy\" > Character name is required"); | |
} | |
@Test | |
public void testIncompleteMovieList() { | |
Movie movie1 = new Movie("The Revenant", ImmutableList.of( | |
new Role(null, "Hugh Glass"), | |
new Role(new Actor("Domhnall", null), "Captain Andrew Henry"), | |
new Role(new Actor("Tom", "Hardy"), null) | |
)); | |
Movie movie2 = new Movie("Bridge of Spies", ImmutableList.of( | |
new Role(null, "Rudolf Abel"), | |
new Role(new Actor("Domenick", null), "Agent Blasco"), | |
new Role(new Actor("Tom", "Hanks"), null) | |
)); | |
final MovieList movies = new MovieList(ImmutableList.of(movie1, movie2)); | |
validate(movies, | |
"Movie \"The Revenant\" > Role \"Hugh Glass\", Actor \"[missing]\" > Actor is required", | |
"Movie \"The Revenant\" > Role \"Captain Andrew Henry\", Actor \"Domhnall [blank]\" > Last Name is required", | |
"Movie \"The Revenant\" > Role \"[missing character name]\", Actor \"Tom Hardy\" > Character name is required", | |
"Movie \"Bridge of Spies\" > Role \"Agent Blasco\", Actor \"Domenick [blank]\" > Last Name is required", | |
"Movie \"Bridge of Spies\" > Role \"Rudolf Abel\", Actor \"[missing]\" > Actor is required", | |
"Movie \"Bridge of Spies\" > Role \"[missing character name]\", Actor \"Tom Hanks\" > Character name is required" | |
); | |
} | |
private <T> void validate(T object, String... expectedMessages) { | |
final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); | |
final ConstraintViolationEnhancer enhancer = new ConstraintViolationEnhancer(); | |
Set<ConstraintViolation<T>> result = validator.validate(object); | |
final List<String> messages = result.stream() | |
.map(enhancer::buildMessageWithPathInformation) | |
.collect(Collectors.toList()); | |
// result.forEach(System.out::println); | |
// messages.forEach(System.out::println); | |
Assert.assertThat(messages, Matchers.containsInAnyOrder(expectedMessages)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment