All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kiwiproject.validation.KiwiConstraintViolations Maven / Gradle / Ivy

Go to download

Kiwi is a utility library. We really like Google's Guava, and also use Apache Commons. But if they don't have something we need, and we think it is useful, this is where we put it.

The newest version!
package org.kiwiproject.validation;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toUnmodifiableMap;
import static java.util.stream.Collectors.toUnmodifiableSet;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.collect.KiwiSets.isNotNullOrEmpty;
import static org.kiwiproject.collect.KiwiSets.isNullOrEmpty;
import static org.kiwiproject.stream.KiwiMultimapCollectors.toLinkedHashMultimap;

import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Path;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.WordUtils;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

/**
 * Static utilities for working with {@link jakarta.validation.ConstraintViolation} objects, generally
 * {@link java.util.Set}s of them.
 * 

* Dependency requirements: *

* The {@code jakarta.validation:jakarta.validation-api} dependency and some implementation such as Hibernate Validator * ({@code org.hibernate.validator:hibernate-validator} must be available at runtime. *

* In addition, currently the "pretty" methods use the {@code #humanize} methods, which rely on {@link WordUtils} from * the commons-text library. So if you use any of these, * you will need to ensure {@code org.apache.commons:commons-text} is available at runtime. */ @UtilityClass public class KiwiConstraintViolations { /** * Convert the set of {@link ConstraintViolation} to an unmodifiable map keyed by the property path. *

* The map's values are the single {@link ConstraintViolation} associated with each property. *

* WARNING: * An {@link IllegalStateException} is thrown if there is more than one violation associated * with any key. Therefore, this method should only be used if you are sure there can only * be at most one violation per property. Otherwise, use either {@link #asMultiValuedMap(Set)} * or {@link #asSingleValuedMap(Set)}. * * @param violations set of non-null but possibly empty violations * @param the type of the root bean that was validated * @return a map whose keys are the property path of the violations, and values are the violations * @throws IllegalStateException if there is more than one violation associated with any key * @see #asSingleValuedMap(Set) * @see #asMultiValuedMap(Set) */ public static Map> asMap(Set> violations) { return asMap(violations, Path::toString); } /** * Convert the set of {@link ConstraintViolation} to an unmodifiable map keyed by the property path. * The property path is determined by the {@code pathTransformer}. *

* The map's values are the single {@link ConstraintViolation} associated with each property. *

* WARNING: * An {@link IllegalStateException} is thrown if there is more than one violation associated * with any key. Therefore, this method should only be used if you are sure there can only * be at most one violation per property. Otherwise, use either {@link #asMultiValuedMap(Set)} * or {@link #asSingleValuedMap(Set)}. * * @param violations set of non-null but possibly empty violations * @param pathTransformer function to convert a Path into a String * @param the type of the root bean that was validated * @return a map whose keys are the property path of the violations, and values are the violations * @throws IllegalStateException if there is more than one violation associated with any key * @see #asSingleValuedMap(Set) * @see #asMultiValuedMap(Set) */ public static Map> asMap(Set> violations, Function pathTransformer) { return violations.stream().collect(toUnmodifiableMap( violation -> pathTransformer.apply(violation.getPropertyPath()), violation -> violation)); } /** * Convert the set of {@link ConstraintViolation} to an unmodifiable map keyed by the property path. *

* The map's values are the last {@link ConstraintViolation} associated with each property. * The definition of "last" depends on the iteration order of the provided set of violations, which * may be non-deterministic if the set does not have a well-defined traversal order. *

* WARNING: * If there is more than one violation associated with any key, the last violation, as * determined by the set traversal order, becomes they key. If you need to retain all violations * associated with each key, use {@link #asMultiValuedMap(Set)}. * * @param violations set of non-null but possibly empty violations * @param the type of the root bean that was validated * @return a map whose keys are the property path of the violations, and values are the violations * @see #asMultiValuedMap(Set) */ public static Map> asSingleValuedMap(Set> violations) { return asSingleValuedMap(violations, Path::toString); } /** * Convert the set of {@link ConstraintViolation} to an unmodifiable map keyed by the property path. * The property path is determined by the {@code pathTransformer}. *

* The map's values are the last {@link ConstraintViolation} associated with each property. * The definition of "last" depends on the iteration order of the provided set of violations, which * may be non-deterministic if the set does not have a well-defined traversal order. *

* WARNING: * If there is more than one violation associated with any key, the last violation, as * determined by the set traversal order, becomes they key. If you need to retain all violations * associated with each key, use {@link #asMultiValuedMap(Set)}. * * @param violations set of non-null but possibly empty violations * @param pathTransformer function to convert a Path into a String * @param the type of the root bean that was validated * @return a map whose keys are the property path of the violations, and values are the violations * @see #asMultiValuedMap(Set) */ public static Map> asSingleValuedMap(Set> violations, Function pathTransformer) { return violations.stream().collect(toUnmodifiableMap( violation -> pathTransformer.apply(violation.getPropertyPath()), violation -> violation, (violation1, violation2) -> violation2)); } /** * Convert the set of {@link ConstraintViolation} to an unmodifiable map keyed by the property path. *

* The map's values are the set of {@link ConstraintViolation} associated with each property. * * @param violations set of non-null but possibly empty violations * @param the type of the root bean that was validated * @return a map whose keys are the property path of the violations, and values are a Set containing * violations for the corresponding property */ public static Map>> asMultiValuedMap(Set> violations) { return asMultiValuedMap(violations, Path::toString); } /** * Convert the set of {@link ConstraintViolation} to an unmodifiable map keyed by the property path. * The property path is determined by the {@code pathTransformer}. *

* The map's values are unmodifiable sets of {@link ConstraintViolation} associated with each property. * * @param violations set of non-null but possibly empty violations * @param pathTransformer function to convert a Path into a String * @param the type of the root bean that was validated * @return a map whose keys are the property path of the violations, and values are a Set containing * violations for the corresponding property */ public static Map>> asMultiValuedMap(Set> violations, Function pathTransformer) { return violations.stream().collect( collectingAndThen( groupingBy(violation -> pathTransformer.apply(violation.getPropertyPath()), toUnmodifiableSet()), Collections::unmodifiableMap)); } /** * Convert the set of {@link ConstraintViolation} to an unmodifiable {@link Multimap} keyed by the property path. * * @param violations set of non-null but possibly empty violations * @param the type of the root bean that was validated * @return a {@link Multimap} whose keys are the property path of the violations, and values contain * the violations for the corresponding property * @implNote The returned value is a {@link com.google.common.collect.LinkedHashMultimap}; the iteration * order of the values for each key is always the order in which the values were added, and there * cannot be duplicate values for a key. */ public static Multimap> asMultimap(Set> violations) { return asMultimap(violations, Path::toString); } /** * Convert the set of {@link ConstraintViolation} to an unmodifiable {@link Multimap} keyed by the property path. * * @param violations set of non-null but possibly empty violations * @param pathTransformer function to convert a Path into a String * @param the type of the root bean that was validated * @return a {@link Multimap} whose keys are the property path of the violations, and values contain * the violations for the corresponding property * @implNote The returned value is a {@link com.google.common.collect.LinkedHashMultimap}; the iteration * order of the values for each key is always the order in which the values were added, and there * cannot be duplicate values for a key. */ public static Multimap> asMultimap(Set> violations, Function pathTransformer) { return violations.stream() .map(violation -> Maps.immutableEntry(pathTransformer.apply(violation.getPropertyPath()), violation)) .collect(collectingAndThen(toLinkedHashMultimap(), ImmutableMultimap::copyOf)); } /** * Convenience method to get the property path of the {@link ConstraintViolation} as a String. *

* Please refer to the Implementation Note for details on the structure of the returned values * and warnings about that structure. * * @param violation the constraint violation * @param the type of the root bean that was validated * @return the property path of the violation, as a String * @implNote This uses {@link ConstraintViolation#getPropertyPath()} to obtain a {@link Path} * and then calls {@link Path#toString()} to get the final value. Therefore, the issues on * {@link Path#toString()} with regard to the structure of the return value apply here as well. * However, in many years of usage, the implementation (in Hibernate Validator anyway) has * always returned the same expected result, and is generally what you expect. *

* The main exception is iterable types, such as Set, that don't have a consistent traversal * order. For example, if you have a property named "nicknames" declared as * {@code Set<@NotBlank String> nicknames}, the property path for violation errors * look like {@code "nicknames[]."}. *

* Maps look similar to Sets. For example, in the Hibernate Validator reference * documentation, one example shows the property path of a constraint violation * on a Map as {@code "fuelConsumption[HIGHWAY]."}, and similarly on * a Map value as {@code "fuelConsumption[]."}. *

* Indexed properties such as a List look more reasonable. For example, suppose a property * named "passwordHints" is declared as {@code List<@NotNull @Valid Hint> passwordHints}, * and that {@code Hint} contains a String property named {@code text}. The property * path for violation errors includes the zero-based index as well as the path. For * example, if the second password hint is not valid, the property path is * {@code passwordHints[1].text}. */ public static String pathStringOf(ConstraintViolation violation) { return violation.getPropertyPath().toString(); } /** * Given a non-empty set of violations, produce a single string containing all violation messages * separated by commas. If the given set is empty (or null), then throw IllegalArgumentException. * * @param violations set of non-empty violations * @param type of object being validated * @return the combined error message * @throws IllegalArgumentException if violations is null or empty */ public static String simpleCombinedErrorMessage(Set> violations) { return combinedErrorMessage(violations, Objects::toString); } /** * Given a set of non-empty violations, produce a single string containing all violation messages separated * by commas. If the given set is empty (or null), then return null. * * @param violations set of violations * @param type of object being validated * @return the combined error message, or null */ public static String simpleCombinedErrorMessageOrNull(Set> violations) { return combinedErrorMessageOrNull(violations, Objects::toString); } /** * Given a set of non-empty violations, produce a single string containing all violation messages separated * by commas. If the given set is empty (or null), then return an empty Optional. * * @param violations set of violations * @param type of object being validated * @return the combined error message, or en empty Optional */ public static Optional simpleCombinedErrorMessageOrEmpty(Set> violations) { return combinedErrorMessageOrEmpty(violations, Objects::toString); } /** * Given a non-empty set of violations, produce a single string containing all violation messages * separated by commas. Each property name is "prettified" by converting {@code camelCase} to sentence case, * for example {@code firstName} becomes "First Name" in the resulting error message. * If the given set is empty (or null), then throw IllegalArgumentException. * * @param violations set of non-empty violations * @param type of object being validated * @return the combined error message * @throws IllegalArgumentException if violations is null or empty */ public static String prettyCombinedErrorMessage(Set> violations) { return combinedErrorMessage(violations, KiwiConstraintViolations::humanize); } /** * Given a non-empty set of violations, produce a single string containing all violation messages * separated by commas. If the given set is empty (or null), then return null. *

* Each property name is "prettified" by converting {@code camelCase} to sentence case, * for example {@code firstName} becomes "First Name" in the resulting error message. * * @param violations set of violations * @param type of object being validated * @return the combined error message, or null */ public static String prettyCombinedErrorMessageOrNull(Set> violations) { return combinedErrorMessageOrNull(violations, KiwiConstraintViolations::humanize); } /** * Given a non-empty set of violations, produce a single string containing all violation messages * separated by commas. If the given set is empty (or null), then return an empty Optional. * * @param violations set of violations * @param type of object being validated * @return the combined error message, or an empty Optional */ public static Optional prettyCombinedErrorMessageOrEmpty(Set> violations) { return combinedErrorMessageOrEmpty(violations, KiwiConstraintViolations::humanize); } /** * Given a non-empty set of violations, produce a single string containing all violation messages * separated by commas. Each property name is transformed using the specified {@code pathTransformer} function. * If the given set is empty (or null), then throw IllegalArgumentException. * * @param violations set of non-empty violations * @param pathTransformer function to convert a Path into a String * @param type of object being validated * @return the combined error message * @throws IllegalArgumentException if violations is null or empty */ public static String combinedErrorMessage(Set> violations, Function pathTransformer) { checkNotNullOrEmpty(violations); checkArgumentNotNull(pathTransformer); return combinedErrorMessageOrEmpty(violations, pathTransformer).orElseThrow(); } /** * Given a non-empty set of violations, produce a single string containing all violation messages * separated by commas. If the given set is empty (or null), then return null. *

* Each property name is transformed using the specified {@code pathTransformer} function. * * @param violations set of violations * @param pathTransformer function to convert a Path into a String * @param type of object being validated * @return the combined error message, or null */ public static String combinedErrorMessageOrNull(Set> violations, Function pathTransformer) { return combinedErrorMessageOrEmpty(violations, pathTransformer).orElse(null); } /** * Given a non-empty set of violations, produce a single string containing all violation messages * separated by commas. If the given set is empty (or null), then return an empty Optional. *

* Each property name is transformed using the specified {@code pathTransformer} function. * * @param violations set of violations * @param pathTransformer function to convert a Path into a String * @param type of object being validated * @return the combined error message, or an empty Optional */ public static Optional combinedErrorMessageOrEmpty(Set> violations, Function pathTransformer) { checkArgumentNotNull(pathTransformer); if (isNullOrEmpty(violations)) { return Optional.empty(); } var result = violations.stream() .map(violation -> KiwiConstraintViolations.propertyAndErrorMessage(violation, pathTransformer)) .sorted() .collect(joining(", ")); return Optional.of(result); } /** * Given a non-empty set of violations, produce a list of strings containing all violation messages. * Each message will contain the property followed by the error message, e.g. "firstName must not be blank". * If the given set is empty (or null), then return an empty list. * * @param violations set of non-empty violations * @param type of object being validated * @return a list of the error messages */ public static List simpleCombinedErrorMessages(Set> violations) { return combinedErrorMessages(violations, Objects::toString); } /** * Given a non-empty set of violations, produce a list of strings containing all violation messages. * Each message will contain the "prettified" property name followed by the error message, e.g., for * a violation on the {@code firstName} property, the message would look like "First Name must not be blank". * If the given set is empty (or null), then return an empty list. * * @param violations set of non-empty violations * @param type of object being validated * @return a list of the error messages */ public static List prettyCombinedErrorMessages(Set> violations) { return combinedErrorMessages(violations, KiwiConstraintViolations::humanize); } /** * Given a non-empty set of violations, produce a list of strings containing all violation messages. * Each message will contain the transformed property name followed by the error message, e.g. * "firstName must not be blank". Each property name is transformed using the specified {@code pathTransformer} * function. If the given set is empty (or null), then return an empty list. * * @param violations set of non-empty violations * @param pathTransformer function to convert a Path into a String * @param type of object being validated * @return a list of the error messages */ public static List combinedErrorMessages(Set> violations, Function pathTransformer) { checkArgumentNotNull(pathTransformer); if (isNullOrEmpty(violations)) { return List.of(); } return violations.stream() .map(violation -> KiwiConstraintViolations.propertyAndErrorMessage(violation, pathTransformer)) .sorted() .toList(); } /** * Given a non-empty set of violations, produce map whose keys are the properties and the corresponding * values are strings containing all violation messages. If the given set is empty (or null), then return an empty * map. * * @param violations set of non-empty violations * @param type of object being validated * @return a map of error messages */ public static Map simpleCombineErrorMessagesIntoMap(Set> violations) { return combineErrorMessagesIntoMap(violations, Objects::toString); } /** * Given a non-empty set of violations, produce map whose keys are the "prettified" properties and the * corresponding values are strings containing all violation messages. If the given set is empty (or null), then * return an empty map. * * @param violations set of non-empty violations * @param type of object being validated * @return a map of error messages */ public static Map prettyCombineErrorMessagesIntoMap(Set> violations) { return combineErrorMessagesIntoMap(violations, KiwiConstraintViolations::humanize); } /** * Given a non-empty set of violations, produce map whose keys are the transformed properties and the * corresponding values are strings containing all violation messages. Each property name is transformed using * the specified {@code pathTransformer} function. If the given set is empty (or null), then return an empty map. * * @param violations set of non-empty violations * @param pathTransformer function to convert a Path into a String * @param type of object being validated * @return a map of error messages */ public static Map combineErrorMessagesIntoMap(Set> violations, Function pathTransformer) { checkArgumentNotNull(pathTransformer); if (isNullOrEmpty(violations)) { return Map.of(); } return violations.stream() .collect(toUnmodifiableMap( violation -> pathTransformer.apply(violation.getPropertyPath()), ConstraintViolation::getMessage, (accumulatedMessage, newErrorMessage) -> accumulatedMessage + ", " + newErrorMessage)); } /** * Transforms the given property path into a human-readable version. Nested paths are separated by * a slash character. Examples: *

    *
  • age becomes Age
  • *
  • firstName becomes First Name
  • *
  • contactInfo.email.address becomes Contact Info / Email / Address
  • *
* * @param propertyPath the property path from a {@link ConstraintViolation} * @return a human-readable path * @throws IllegalArgumentException if either argument is null */ public static String humanize(Path propertyPath) { return humanize(propertyPath, "/"); } /** * Transforms the give property path into a human-readable version. Nested paths are separated by * the given {@code pathSeparator}. *

* For example, contactInfo.email.address using ":" as the path separator would result in Contact Info:Email:Address. * * @param propertyPath the property path from a {@link ConstraintViolation} * @param pathSeparator the separator to use between path elements * @return a human-readable path * @throws IllegalArgumentException if either argument is null */ public static String humanize(Path propertyPath, String pathSeparator) { checkArgumentNotNull(propertyPath, "propertyPath must not be null"); checkArgumentNotNull(pathSeparator, "pathSeparator must not be null"); var splat = StringUtils.splitByCharacterTypeCamelCase(propertyPath.toString()); var joined = Arrays.stream(splat) .map(str -> ".".equals(str) ? pathSeparator : str) .collect(joining(" ")); return WordUtils.capitalize(joined); } private static void checkNotNullOrEmpty(Set> violations) { checkArgument(isNotNullOrEmpty(violations), "There are no violations to combine"); } private static String propertyAndErrorMessage(ConstraintViolation violation, Function pathTransformer) { return pathTransformer.apply(violation.getPropertyPath()) + " " + violation.getMessage(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy