How to write (type level annotation) custom annotation to allow a choice validation (exactly one of a number of properties has to be not null)?
Even if this "question" is very broad I will give an answer as I have exactly the needed piece of code here:
#Target(ElementType.TYPE)
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = ChoiceValidator.class)
public #interface Choice {
String[] value();
boolean multiple() default false;
String message() default "{com.stackoverflow.validation.Choice.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Here a validator using a bean property access:
public class ChoiceValidator implements ConstraintValidator<Choice, Object> {
private String[] properties;
private boolean allowMultiple;
#Override
public void initialize(Choice constraintAnnotation) {
if (constraintAnnotation.value().length < 2) {
throw new IllegalArgumentException("at least two properties needed to make a choice");
}
properties = constraintAnnotation.value();
allowMultiple = constraintAnnotation.multiple();
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
try {
BeanInfo info = getBeanInfo(value.getClass());
long notNull = Stream.of(properties)
.map(property -> Stream.of(info.getPropertyDescriptors())
.filter(desr -> desr.getName().equals(property))
.findAny()
.orElse(null)
)
.map(prop -> getProperty(prop, value))
.filter(Objects::nonNull)
.count();
return allowMultiple ? notNull != 0 : notNull == 1;
} catch (IntrospectionException noBean) {
return false;
}
}
private Object getProperty(PropertyDescriptor prop, Object bean) {
try {
return prop.getReadMethod() == null ? null : prop.getReadMethod().invoke(bean);
} catch (ReflectiveOperationException noAccess) {
return null;
}
}
}
A typical usage can look like this (lombok annotation to generate getters and setters):
#Data
#Choice({"one", "two"})
class OneOf {
private String one;
private String two;
private String whatever;
}
#Data
#Choice(value = {"one", "two"}, multiple = true)
class AnyOf {
private String one;
private String two;
}
But to clarify: Stackoverflow is a QA community for developers to exchange knowledge. It is not a place to ask "Can you please code this for me for free?". All valuable contributors at least try to solve problems first and come with detailed questions afterwards. People answering questions spend their spare time and are not payed for. Please show respect by asking for specific problems and showing own efforts in the future.
Related
I'm currently working on a custom ConstraintValidator to check an array of objects which have a timespan associated with them for overlaps in their timespan. The validation logic is working, however, I am uncertain how to add a "This object's timeslot overlaps with another object's timeslot" message to every object in violation of the validation logic.
I've tried several approaches described here:
https://docs.oracle.com/javaee/7/api/javax/validation/ConstraintValidatorContext.html
Specifically those described in the buildConstraintViolationWithTemplate method docs.
Here is the relevant section of the code:
#Override
public boolean isValid(List<Shift> shifts, ConstraintValidatorContext context) {
List<Integer> overlappingShiftIndices = determineOverlappingShifts(shifts);
if (!overlappingShiftIndices.isEmpty()) {
log.debug("Overlap validation failed.");
context.disableDefaultConstraintViolation();
// Moving the error from form-level to fields
for (int index : overlappingShiftIndices) {
context.buildConstraintViolationWithTemplate("{com.generali.standbyscheduler.validation.shiftlist.overlap}")
.addBeanNode()
.inIterable().atIndex(index)
.addConstraintViolation();
}
return false;
}
log.debug("Overlap validation succeeded.");
return true;
}
As you can see I tried the .addBeanNode().inIterable().atIndex(index) approach here. When looking at the ConstraintViolations the property path displays as list[index]. Is this correct?
I plan on using this to access the determined violations from a BindingResult in a Thymeleaf template and am uncertain whether the violations will be accessible this way. The list will be a property of another bean, so I'm expecting to read the violations using a path like propertyNameOfList[index]. Or would it be propertyNameOfList.list[index] or something else?
I got the same problem, when trying to validate if certain fields are unique within a object list. My own solution (I found none on the internet :/ ):
You have to overwrite the current PropertyNode and add the index number by using .addPropertyNode(null).inIterable().atIndex(index). Example:
ConstraintAnnotation:
#Target({ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = UniqueBusinessIndexValidator.class)
public #interface UniqueEntries {
String message() default ValidationMessages.REQUIRED_UNIQUE_INDEX;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ConstraintValidator:
public class UniqueBusinessIndexValidator implements ConstraintValidator<UniqueEntries, List<HasBusinessIndex>> {
#Override
public boolean isValid(List<HasBusinessIndex> collection, ConstraintValidatorContext context) {
if (collection == null || collection.isEmpty()) {
return true;
}
Map<String, List<Integer>> indexesMap = new HashMap<>();
for (int runner = 0; runner < collection.size(); runner++) {
String businessIndex = collection.get(runner).getBusinessIndex();
if (indexesMap.containsKey(businessIndex)) {
indexesMap.get(businessIndex).add(runner);
} else {
indexesMap.put(businessIndex, new ArrayList<>(List.of(runner)));
}
}
boolean isValid = indexesMap.values().stream().noneMatch(indexes -> indexes.size() > 1);
if (!isValid) {
indexesMap.values()
.stream()
.filter(index -> index.size() > 1)
.forEach(index -> addUniqueBusinessIndexkennungViolation(context, index));
}
return isValid;
}
private void addUniqueBusinessIndexkennungViolation(ConstraintValidatorContext context, List<Integer> indexes) {
for (Integer index : indexes) {
context.buildConstraintViolationWithTemplate(ValidationMessages.REQUIRED_UNIQUE_INDEX)
.addPropertyNode(null)
.inIterable()
.atIndex(index)
.addPropertyNode("businessIndex")
.addConstraintViolation()
.disableDefaultConstraintViolation();
}
}
At my Spring/Boot Java project I have a set of service methods, for example like a following one:
#Override
public Decision create(String name, String description, String url, String imageUrl, Decision parentDecision, Tenant tenant, User user) {
name = StringUtils.trimMultipleSpaces(name);
if (org.apache.commons.lang3.StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Decision name can't be blank");
}
if (!org.apache.commons.lang3.StringUtils.isEmpty(url) && !urlValidator.isValid(url)) {
throw new IllegalArgumentException("Decision url is not valid");
}
if (!org.apache.commons.lang3.StringUtils.isEmpty(imageUrl) && !urlValidator.isValid(imageUrl)) {
throw new IllegalArgumentException("Decision imageUrl is not valid");
}
if (user == null) {
throw new IllegalArgumentException("User can't be empty");
}
if (tenant != null) {
List<Tenant> userTenants = tenantDao.findTenantsForUser(user.getId());
if (!userTenants.contains(tenant)) {
throw new IllegalArgumentException("User doesn't belong to this tenant");
}
}
if (parentDecision != null) {
if (tenant == null) {
if (findFreeChildDecisionByName(parentDecision.getId(), name) != null) {
throw new EntityAlreadyExistsException("Parent decision already contains a child decision with a given name");
}
} else {
if (findTenantedChildDecisionByName(parentDecision.getId(), name, tenant.getId()) != null) {
throw new EntityAlreadyExistsException("Parent decision already contains a child decision with a given name");
}
}
Tenant parentDecisionTenant = tenantDao.findTenantForDecision(parentDecision.getId());
if (parentDecisionTenant != null) {
if (tenant == null) {
throw new IllegalArgumentException("Public decision cannot be added as a child to tenanted parent decision");
}
if (!parentDecisionTenant.equals(tenant)) {
throw new IllegalArgumentException("Decision cannot belong to tenant other than parent decision tenant");
}
} else {
if (tenant != null) {
throw new IllegalArgumentException("Tenanted decision cannot be added as a child to public parent decision");
}
}
} else {
if (tenant == null) {
if (findFreeRootDecisionByName(name) != null) {
throw new EntityAlreadyExistsException("Root decision with a given name already exists");
}
} else {
if (findTenantedRootDecisionByName(name, tenant.getId()) != null) {
throw new EntityAlreadyExistsException("Root decision with a given name for this tenant already exists");
}
}
}
Decision decision = createOrUpdate(new Decision(name, description, url, imageUrl, parentDecision, user, tenant));
if (parentDecision != null) {
parentDecision.addChildDecision(decision);
}
criterionGroupDao.create(CriterionGroupDaoImpl.DEFAULT_CRITERION_GROUP_NAME, null, decision, user);
characteristicGroupDao.create(CharacteristicGroupDaoImpl.DEFAULT_CHARACTERISTIC_GROUP_NAME, null, decision, user);
return decision;
}
As you can see, most of the code lines from this method are occupied with a validation logic and I continue to adding a new validation cases there.
I want to refactor this method and move validation logic outside of this method in a more appropriate places. Please suggest how it can be done with Spring framework.
As chrylis mentioned in the comments, you can achieve this goal by using JSR-303 bean validation. The first step is to create a class that contains your input parameters:
public class DecisionInput {
private String name;
private String description;
private String url;
private String imageUrl;
private Decision parentDecision;
private Tenant tenant;
private User user;
// Constructors, getters, setters, ...
}
After that, you can start adding validation annotations, for example:
public class DecisionInput {
#NotEmpty
private String name;
#NotEmpty
private String description;
#NotEmpty
private String url;
#NotEmpty
private String imageUrl;
private Decision parentDecision;
private Tenant tenant;
#NotNull
private User user;
// Constructors, getters, setters, ...
}
Be aware that the #NotEmpty annotation is not a standard JSR-303 annotation, but a Hibernate annotation. If you prefer using standard JSR-303 you can always create your own custom validator. For your tenant and your decision, you certainly need a custom validator. First of all create an annotation (eg #ValidTenant). On your annotation class, make sure to add the #Constraint annotation, for example:
#Constraint(validatedBy = TenantValidator.class) // Your validator class
#Target({ TYPE, ANNOTATION_TYPE }) // Static import from ElementType, change this to METHOD/FIELD if you want to create a validator for a single field (rather than a cross-field validation)
#Retention(RUNTIME) // Static import from RetentionPolicy
#Documented
public #interface ValidTenant {
String message() default "{ValidTenant.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Now you have to create the TenantValidator class and make it implement ConstraintValidator<ValidTenant, DecisionInput>, for example:
#Component
public class TenantValidator implements ConstraintValidator<ValidTenant, DecisionInput> {
#Autowired
private TenantDAO tenantDao;
#Override
public void initialize(ValidTenant annotation) {
}
#Override
public boolean isValid(DecisionInput input, ConstraintValidatorContext context) {
List<Tenant> userTenants = tenantDao.findTenantsForUser(input.getUser().getId());
return userTenants.contains(input.getTenant());
}
}
The same can be done for the validation of the parent decision. Now you can just refactor your service method to this:
public Decision create(#Valid DecisionInput input) {
// No more validation logic necessary
}
If you want to use your own error messages, I suggest reading this answer. Basically you create a ValidationMessages.properties file and put your messages there.
first of all thanks for everyone who tries to help me with this: I'm trying to validate a list with at least one element, if I check using
Set<ConstraintViolation<Object>> constraintViolations = VALIDATOR.validate(anObj);
The constraint works fine, but when I try to persist, the object with one element pass again and the list is null...
This is the part of the class with the list:
public class Team implements Serializable {
//...
#AtLeastOneNotNull
#ManyToMany
#JoinTable(
name="teams_players"
, joinColumns={
#JoinColumn(name="id_team")
}
, inverseJoinColumns={
#JoinColumn(name="id_player")
}
)
private List<Player> players;
//...
}
The validator:
public class AtLeastOneNotNullValidator implements ConstraintValidator<AtLeastOneNotNull, List<?>> {
#Override
public void initialize(AtLeastOneNotNull constraint) {
}
#Override
public boolean isValid(List<?> aCollection, ConstraintValidatorContext aConstraintValidatorContext) {
if(aCollection == null || aCollection.isEmpty())
return false;
else
return true;
}
}
Annotation:
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Constraint(validatedBy = AtLeastOneNotNullValidator.class)
public #interface AtLeastOneNotNull {
String message() default "{[ERROR] Collection must have at least one element}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Test class:
public class TeamDAOTest {
private static Validator VALIDATOR;
TeamTestHelper teamTestHelper = null;
TeamDAO teamDaoTest = null;
#Before
public void initTestClass() {
teamDaoTest = new TeamDAO();
teamTestHelper = new TeamTestHelper();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
VALIDATOR = factory.getValidator();
}
#After
public void endTestClass() {
}
#Test
public void savingRightTeam() {
Team testTeam = teamTestHelper.getTeamOK();
// Set<ConstraintViolation<Team>> constraintViolations = VALIDATOR.validate(testTeam);
//
// assertEquals(0, constraintViolations.size());
//
assertEquals("Inserting right Team", true, teamDaoTest.updateEntity(testTeam));
}
#Test
public void savingWrongTeam() {
Team testTeam = teamTestHelper.getTeamKO_WithoutPlayers();
// Set<ConstraintViolation<Team>> constraintViolations = VALIDATOR.validate(testTeam);
//
// assertEquals(1, constraintViolations.size());
assertEquals("Inserting empty Team", false, teamDaoTest.updateEntity(testTeam));
}
}
And finally, this is what happens when I run this
First of all, you can see the Team created with the list with one element.
Note: The helper creates a player and persist it first, then adds to a team and return it, and this is the Team that we can see.
In a second step, when it's trying to persist the element and gets through the validator, the list is empty... ¿why?
And finally, of course, with a null list, the merge method throws an exception:
Any idea of what could be happening? I don't know why this happens, as you can see, I commented the other lines, the test pass ok, but not when I try to update, it seems as another object, or as the object instantiates again
Thanks again to everyone
EDIT:
The getPlayer() and addPlayer() methods in Team object:
public List<Player> getPlayers() {
if(this.players == null)
this.players = new ArrayList<Player>();
return this.players;
}
public boolean addPlayer(Player aPlayer){
if(!getPlayers().contains(aPlayer)){
this.players.add(aPlayer);
return true;
}
else return false;
}
Edit 2: DAO, as you can see, by now I don't need more specific methods in TeamDAO
public class TeamDAO extends AbstractDAOLayer<Team> {
}
UpdateEntity method in AbstractDAOLayer
public boolean updateEntity(T anObject){
try{
this.beginTransaction();
this.entityManager.merge(anObject);
this.finishtTransaction();
return true;
}
catch(Exception e){
return false;
}
}
Please check this. The problem has nothing to do with BV but rather with jpa merge semantic with many-to-many relationship.
I'm using the Hibernate #NotNull validator, and I'm trying to create custom messages to tell the user which field has generated the error when it is null. Something like this:
notNull.custom = The field {0} can't be null.
(this will be in my ValidationMessages.properties file).
Where the {0} should be the field name passed to the validator this way:
#NotNull(field="field name")
There is any way I can do that?
To customize your annotation message you need to disable the existing violation message inside isValid() method and build a new violation message and add it.
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
In the given below example, I am creating an annotation for input date validation on the basis of "invalid date", "can't be greater than today date" and "date format is correct or not".
#CheckDateIsValid(displayPattern = "DD/MM/YYYY", programPattern = "dd/MM/yyyy", groups = Order2.class)
private String fromDate;
Annotation Interface -
public #interface CheckDateIsValid {
String message() default "default message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String displayPattern();
String programPattern();
}
Annotation implementation Class -
public class CheckDateIsValidValidator implements ConstraintValidator<CheckDateIsValid, String> {
#Value("${app.country.timeZone}")
private String timeZone;
private String displayPattern;
private String programPattern;
#Override
public void initialize(CheckDateIsValid constraintAnnotation) {
this.displayPattern = constraintAnnotation.displayPattern();
this.programPattern = constraintAnnotation.programPattern();
}
#Override
public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
try {
// disable existing violation message
constraintContext.disableDefaultConstraintViolation();
if (object == null) {
return true;
}
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(programPattern);
LocalDateTime time = LocalDate.parse(object, formatter).atStartOfDay();
ZoneOffset zoneOffSet = ZoneOffset.of(timeZone);
OffsetDateTime todayDateTime = OffsetDateTime.now(zoneOffSet);
if (time == null) {
customMessageForValidation(constraintContext, "date is not valid");
return false;
} else if (todayDateTime.isBefore(time.atOffset(zoneOffSet))) {
customMessageForValidation(constraintContext, "can't be greater than today date");
return false;
}
return time != null;
} catch (Exception e) {
customMessageForValidation(constraintContext, "date format should be like " + displayPattern);
return false;
}
}
private void customMessageForValidation(ConstraintValidatorContext constraintContext, String message) {
// build new violation message and add it
constraintContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
}
}
If your requirement can be satisfied by interpolating hibernate messages, so you can create/name your *property file like that:
ValidationMessages.properties
And inside that:
javax.validation.constraints.NotNull.message = CUSTOMIZED MESSAGE WHEN NOTNULL is violated!
Hibernate by default searches a ResourceBundle named ValidationMessages. Also a locale might be involved: ValidationMessages_en, ValidationMessages_de, <..>
Hibernate will provide your customized message through interpolatedMessage parameter, so all ConstraintViolationException relevant information (included your message ) will be showed. So you message will be a part of real exception. Some unwieldy information will be provided!
If you want to make your custom exception (without default ConstraintViolationException behavior) check this out:
Using GenericDao concept, consider the following
public void saveOrUpdate(IEntity<?> entity) {
try {
if(entity.getId == null) {
em.persist(entity);
} else {
em.merge(entity)l
}
} catch(ConstraintViolationException cve) {
throw new ConstraintViolationEx(constructViolationMessage(cve.getConstraintViolations()));
}
}
private String constructMessage(Set<ConstraintViolation<?>> pConstraintViolations) {
StringBuilder customMessages = new StringBuilder();
for(ConstraintViolation<?> violation : pConstraintViolations) {
String targetAnnotation = violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName();
if(supportsCustomMessage(targetAnnotation)) {
applyMessage(violation, targetAnnotation, customMessages);
} else {
// do something with not customized constraints' messages e.g. append it to existing container
}
}
return customMessages.toString();
}
private void applyMessage(ConstraintViolation<?> pViolation, String pTargetAnnotation, StringBuilder pCustomMessages) {
String targetClass = pViolation.getRootBean().getClass().getName();
String targetField = pViolation.getPropertyPath().toString();
pCustomMessages.append(MessageFormat.format(getMessageByAnnotation(pTargetAnnotation), targetClass, targetField));
pCustomMessages.append(System.getProperty("line.separator"));
}
private String getBundleKey() {
return "ValidationMessages"; //FIXME: hardcoded - implement your true key
}
private String getMessageByAnnotation(String pTargetAnnotation) {
ResourceBundle messages = ResourceBundle.getBundle(getBundleKey());
switch(pTargetAnnotation) {
case "NotNull":
return messages.getString(pTargetAnnotation + ".message");
default:
return "";
}
}
private boolean supportsCustomMessage(String pTargetAnnotation) {
return customizedConstraintsTypes.contains(pTargetAnnotation);
}
Produced result:
test.model.exceptions.ConstraintViolationEx
test.model.Person : name cannot be null
test.model.Person : surname cannot be null
A hibernate ConstraintViolation provides relevant information about root class and restricted field. As you see, it applies for all hibernate supported constraints, so you need to check if current annotation can be customized by supportsCustomMessage(<..>)! If it can (it's up to you), you should get appropriate message by constraint annotation doing `getMessageByAnnotation(<..>)'.
All you need to do is implement not supported constraints logic. For example it can append it's cause message or interpolated with default message (and true exception goes to *log file)
I don't understand how I can retrieve the Enum values in an annotation processor.
My annotation is a custom Java Bean Validation annotation:
#StringEnumeration(enumClass = UserCivility.class)
private String civility;
On my annotation processor, I can access to instances of these:
javax.lang.model.element.AnnotationValue
javax.lang.model.type.TypeMirror
javax.lang.model.element.TypeElement
I know it contains the data about my enum since I can see that in debug mode. I also see ElementKind == Enum
But I want to get all the names for that Enum, can someone help me please.
Edit: I don't have access to the Class object of this Enum, because we are in an annotation processor, and not in standart Java reflection code. So I can't call Class#getEnumConstants() or EnumSet.allOf(MyEnum.class) unless you tell me how I can get the Class object from the types mentioned above.
I found a solution (this uses Guava):
class ElementKindPredicate<T extends Element> implements Predicate<T> {
private final ElementKind kind;
public ElementKindPredicate(ElementKind kind) {
Preconditions.checkArgument(kind != null);
this.kind = kind;
}
#Override
public boolean apply(T input) {
return input.getKind().equals(kind);
}
}
private static final ElementKindPredicate ENUM_VALUE_PREDICATE = new ElementKindPredicate(ElementKind.ENUM_CONSTANT);
public static List<String> getEnumValues(TypeElement enumTypeElement) {
Preconditions.checkArgument(enumTypeElement.getKind() == ElementKind.ENUM);
return FluentIterable.from(enumTypeElement.getEnclosedElements())
.filter(ENUM_VALUE_PREDICATE)
.transform(Functions.toStringFunction())
.toList();
}
The answer given by Sebastian is correct, but if you're using Java 8 or above, you can use the following (cleaner) approach than using Google Guava.
List<String> getEnumValues(TypeElement enumTypeElement) {
return enumTypeElement.getEnclosedElements().stream()
.filter(element -> element.getKind().equals(ElementKind.ENUM_CONSTANT))
.map(Object::toString)
.collect(Collectors.toList());
}
Here's a complete example. Note the use of getEnumConstants on the enum values.
public class Annotate {
public enum MyValues {
One, Two, Three
};
#Retention(RetentionPolicy.RUNTIME)
public #interface StringEnumeration {
MyValues enumClass();
}
#StringEnumeration(enumClass = MyValues.Three)
public static String testString = "foo";
public static void main(String[] args) throws Exception {
Class<Annotate> a = Annotate.class;
Field f = a.getField("testString");
StringEnumeration se = f.getAnnotation(StringEnumeration.class);
if (se != null) {
System.out.println(se.enumClass());
for( Object o : se.enumClass().getClass().getEnumConstants() ) {
System.out.println(o);
}
}
}
}
This will print out:
Three
One
Two
Three