Skip to content

Instantly share code, notes, and snippets.

@phaas
Last active February 22, 2016 04:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phaas/94b37d579b6eff9d1d60 to your computer and use it in GitHub Desktop.
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)
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));
}
}
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));
}
}
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();
}
}
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