I created a custom annotation called CrudSearchable and have defined some attributes there. However, the attributes I am assigning are already visible from the bean. Is there a way I can grab these values without having to redefine them manually?
// Bean
public class MockUser {
#CrudSearchable(attribute = "name",
parentClass = MockUser.class)
private String name;
#CrudSearchable(attribute = "group",
parentClass = MockUser.class,
mappedClass = MockGroup.class)
private MockGroup group;
// Get/Set, Equals/Hashcode, etc...
}
// Annotation Class
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface CrudSearchable {
String attribute();
boolean searchable() default true;
Class<?> mappedClass() default CrudSearchable.class;
Class<?> parentClass() default Object.class;
}
Where attribute is the attribute name, parentClass is the class literal using the annotation, and mapped class is the nested class object if applicatable.
MockUser obj = new MockUser();
Class<?> c = obj.getClass();
Field[] fields = c.getDeclaredFields();
CrudSearchable annotation = fields[0].getAnnotation(CrudSearchable.class);
System.out.println("attribute: " + annotation.attribute() +
"searchable: " + annotation.searchable());
Hope this helps
I found the answer on another Question. This is what I was looking for.
// Put in Reflector.java
/**
* Changes the annotation value for the given key of the given annotation to newValue and returns
* the previous value.
*/
#SuppressWarnings("unchecked")
public static Object changeAnnotationValue(Annotation annotation, String key, Object newValue){
Object handler = Proxy.getInvocationHandler(annotation);
Field f;
try {
f = handler.getClass().getDeclaredField("memberValues");
} catch (NoSuchFieldException | SecurityException e) {
throw new IllegalStateException(e);
}
f.setAccessible(true);
Map<String, Object> memberValues;
try {
memberValues = (Map<String, Object>) f.get(handler);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
Object oldValue = memberValues.get(key);
if (oldValue == null || oldValue.getClass() != newValue.getClass()) {
throw new IllegalArgumentException();
}
memberValues.put(key,newValue);
return oldValue;
}
Once I have this I can call the method below in the Constructor to alter the annotations I use.
// Put in Service Class
public void modifySearchable(Class<?> clazz) {
for(Field f : clazz.getDeclaredFields()){
CrudSearchable[] searchableArray = f.getDeclaredAnnotationsByType(CrudSearchable.class);
for(CrudSearchable searchable : searchableArray){
if(searchable == null){
continue;
}
Reflector.alterAnnotation(searchable, "attribute", f.getName());
Reflector.alterAnnotation(searchable, "parentClass", clazz);
if(!(searchable.mappedAttribute().equals(""))){
String mappedGetter = "get" +
searchable.mappedAttribute().substring(0, 1).toUpperCase() +
searchable.mappedAttribute().substring(1);
Reflector.alterAnnotation(searchable, "mappedClass", f.getType());
Reflector.alterAnnotation(searchable, "mappedGetter", mappedGetter);
}
}
}
}
// Changed Bean
#Id
#GeneratedValue
#Column(name = "MOCK_USER_ID")
private Long id;
#Column(name = "MOCK_USER_NAME")
#CrudSearchable
private String name;
#ManyToOne
#JoinColumn(name = "MOCK_GROUP", nullable = false)
#CrudSearchable(mappedAttribute = "name")
private MockGroup group;
public MockUser(){
super();
new Searchable<>().modifySearchable(this.getClass());
}
Seems like a lot to change the values instead of having the user define them, but I believe that it will make the code more user friendly.
Hope this helps someone. I found the answer on this post: Modify a class definition's annotation string parameter at runtime. Check it out!
Related
I'm trying to update my DTO with Reflection. The problem is that some fields in my DTO are enums and I get an error that I can not set the enum field to String.
DTO:
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Data
#Builder
#Table(name = "xxx")
public class Model {
#Id
#Column(name = "id")
private String runId;
#Column(name = "name")
private String name;
#Column(name = "status")
#Enumerated(EnumType.STRING)
private ExecutionStatus status;
}
Controller:
#PatchMapping(path = "/{id}", consumes = "application/json")
public ResponseEntity<Void> partialUpdateModel(#PathVariable String id, #RequestBody Map<Object, Object> fields)
throws Exception {
Optional<Model> model= service.getById(id);
if (model.isPresent()) {
fields.forEach((key, value) -> {
Field field = ReflectionUtils.findField(Model.class, (String) key);
field.setAccessible(true);
ReflectionUtils.setField(field, model.get(), value);
});
So when it comes to the enum field, the field can not be set. It says
cannot set ExecutionStatus to String
What you are trying to do is this:
model.status = "STATUS_1";
// incompatible types: java.lang.String cannot be converted to so.A.ExecutionStatus
What you apparently want to do is finding the enum constant of the specified enum type with the specified string. That's what the Enum.valueOf or YourEnum.valueOf method is doing.
Example:
enum ExecutionStatus {
STATUS_1,
STATUS_2,
}
static class Model {
public String runId;
public String name;
public ExecutionStatus status;
#Override
public String toString() {
return "Model{" +
"runId='" + runId + '\'' +
", name='" + name + '\'' +
", status=" + status +
'}';
}
}
public static void main(String[] args) {
Map<Object, Object> fields = Map.of(
"runId", "MyRunId",
"name", "MyName",
"status", "STATUS_1"
);
Model model = new Model();
fields.forEach((key, value) -> {
Field field = null;
try {
field = Model.class.getDeclaredField((String) key);
field.setAccessible(true);
if (field.getType().isEnum()) {
// First variant (YourEnum.valueOf(String)
Method valueOf = field.getType().getMethod("valueOf", String.class);
Object enumConstant = valueOf.invoke(null, value);
field.set(model, enumConstant);
// Alternative (Enum.valueOf(Class, String) (cast is safe due to isEnum)
field.set(model, Enum.valueOf((Class<Enum>) field.getType(), (String) value));
} else {
field.set(model, value);
}
} catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
});
System.out.println(model);
}
You can do like this, Also understand reflection is costly it involves types that are being dynamically resolved, i'd recommend writing setters or constructor instead
if ("ExecutionStatus".equalsIgnoreCase(field.getType().getSimpleName())) {
ReflectionUtils.setField(field, model.get(), ExecutionStatus.valueOf(value));
} else {
ReflectionUtils.setField(field, model.get(), value);
}
This is because your source map is of type <Object, Object>
What you try to do is to set a field of type ExecutionStatus reading a value of type Object. Types on your fields must match. First convert a value to ExecutionStatus then use the method .setField(..).
I have some classes like below:
#Getter
#Setter
class Person{
#JsonProperty("cInfo")
private ContactInformation contactInfo;
private String name;
private String position;
}
#Getter
#Setter
class ContactInformation{
#JsonProperty("pAddress")
private Address address;
}
#Getter
#Setter
class Address{
private String street;
private String district;
}
And what I am going to do is writing an Utils method for the Person object that take one parameter which is the attributeName as String and return the getter value for this attribute.
Ex:
attributeName = name -> return person.getName()
attributeName = position -> return person.getPosition()
attributeName = cInfo.pAddress.street -> return person.getContactInfo().getAddress().getStreet()
attributeName = cInfo.pAddress.district -> return person.getContactInfo().getAddress().getDistrict()
Below is what I've done: I loop through all the fields in the Person object and check if the attributeName equal to either the JsonProperty's Name or the Field's Name then I will return this getter.
Object result;
Field[] fields = Person.class.getDeclaredFields();
for (Field field : fields) {
JsonProperty jsonProperty = field.getDeclaredAnnotation(JsonProperty.class);
if (jsonProperty != null && jsonProperty.value().equals(attributeName)) {
result = Person.class.getMethod("get" + capitalize(field.getName())).invoke(person);
} else {
if (field.getName().equals(attributeName)) {
result = person.class.getMethod("get" + capitalize(field.getName()))
.invoke(person);
}
}
}
This worked but only with the fields that locate direct in the Person class, ex: name, position. With the fields inside of contactInfo or address I am still getting stuck there. Can anyone give me some hint here how can I do it?
Thank you!
Because path like a.b.c related to different objects. So you need to. split by point and for each token call get and use obtained result for next token
UPDATE: something like:
private static Object invkGen(Object passedObj, String attributeName) throws Exception {
final String[] split = attributeName.split("\\.");
Object result = passedObj;
for (String s : split) {
if (result == null) {
break;
}
result = invk(result, s);
}
return result;
}
private static Object invk(Object passedObj, String attributeName) throws Exception {
Object result = null;
final Field[] fields = passedObj.getClass().getDeclaredFields();
for (Field field : fields) {
JsonProperty jsonProperty = field.getDeclaredAnnotation(JsonProperty.class);
if (jsonProperty != null && jsonProperty.value().equals(attributeName)) {
result = Person.class.getMethod("get" + capitalize(field.getName())).invoke(passedObj);
} else {
if (field.getName().equals(attributeName)) {
result = passedObj.getClass().getMethod("get" + capitalize(field.getName()))
.invoke(passedObj);
}
}
}
return result;
}
Is it possible to access a field value, where field name is described in annotation which annotate another field in class.
For example:
#Entity
public class User {
#NotBlank
private String password;
#Match(field = "password")
private String passwordConfirmation;
}
Annotation:
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = FieldMatchValidator.class)
#Documented
public #interface Match {
String message() default "{}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String field();
}
Now, is it possible to access field password from class User in ConstraintValidator implementaion class?
Edit:
I wrote something like this:
public class MatchValidator implements ConstraintValidator<Match, Object> {
private String mainField;
private String secondField;
private Class clazz;
#Override
public void initialize(final Match match) {
clazz = User.class;
final Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Match.class)) {
mainField = field.getName();
}
}
secondField = match.field();
}
#Override
public boolean isValid(final Object value, final ConstraintValidatorContext constraintValidatorContext) {
try {
Object o; //Now how to get the User entity instance?
final Object firstObj = BeanUtils.getProperty(o, mainField);
final Object secondObj = BeanUtils.getProperty(o, secondField);
return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
} catch (final Exception ignore) {
ignore.printStackTrace();
}
return true;
}
}
Now the question is how can I get the User object instance and compare fields values?
#Hardy Thenks for tip. Finally wrote some code which matches (more or less) expected result.
I'll paste it here, maybe will help someone to solve his problem.
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Match {
String field();
String message() default "";
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = MatchValidator.class)
#Documented
public #interface EnableMatchConstraint {
String message() default "Fields must match!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class MatchValidator implements ConstraintValidator<EnableMatchConstraint, Object> {
#Override
public void initialize(final EnableMatchConstraint constraint) {}
#Override
public boolean isValid(final Object o, final ConstraintValidatorContext context) {
boolean result = true;
try {
String mainField, secondField, message;
Object firstObj, secondObj;
final Class<?> clazz = o.getClass();
final Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Match.class)) {
mainField = field.getName();
secondField = field.getAnnotation(Match.class).field();
message = field.getAnnotation(Match.class).message();
if (message == null || "".equals(message))
message = "Fields " + mainField + " and " + secondField + " must match!";
firstObj = BeanUtils.getProperty(o, mainField);
secondObj = BeanUtils.getProperty(o, secondField);
result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
if (!result) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation();
break;
}
}
}
} catch (final Exception e) {
// ignore
//e.printStackTrace();
}
return result;
}
}
And how to use it...? Like this:
#Entity
#EnableMatchConstraint
public class User {
#NotBlank
private String password;
#Match(field = "password")
private String passwordConfirmation;
}
You either need to write a class level constraint in which you get the full User instance passed into the isValid call or you can use something like #ScriptAssert.
At the moment it is not possible to access the root bean instance as part of a "normal" field validation. There is a BVAL issue - BVAL-237 - which discusses to add this functionality, but so far it is not yet part of the Bean Validation specification.
Note, there are good reasons why the root bean is not accessible atm. Constraints which rely on the root bean being accessible will fail for the validateValue case.
Hack around... Hack because these internal Hibernate implementations won't work when they migrate to Java modules.
#Override
public boolean isValid(final Serializable value, final ConstraintValidatorContext context) {
var context = (ConstraintValidatorContextImpl) context;
//no-inspection unchecked
var descriptor = (ConstraintDescriptorImpl<Exists>) context.getConstraintDescriptor();
var existsAnnotation = descriptor.getAnnotationDescriptor().getAnnotation();
// Removed b/c irrelevant
}
I have a class named ClBranch.java like below:
#Entity
#Table(name = "PROVINCE")
public class PROVINCE implements Serializable {
#Id
#Column(name="PR_CODE", length = 50)
private String provinceCode
#Column(name="PR_NAME", length = 500)
private String provinceName
......
getter-setter.
}
This is my code:
public static String getClassAnnotationValue(Class classType, Class annotationType, String attributeName) {
String value = null;
Annotation annotation = classType.getAnnotation(annotationType);
if (annotation != null) {
try {
value = (String) annotation.annotationType().getMethod(attributeName).invoke(annotation);
} catch (Exception ex) {
ex.printStackTrace();
}
}
return value;
}
String columnName = getClassAnnotationValue(PROVINCE .class, Column.class, "name");
By this way, I only get ColumnName as PROVINCE. I can not get ColumnName. How can I do it?
The #Column annotation is defined on the fields, not on the class. So you must query annotation values from the private fields:
String columnName = getAnnotationValue(PROVINCE.class.getDeclaredField("provinceCode"), Column.class, "name");
To be able to pass Field objects to your method, change the type of your classType parameter from Class to AnnotatedElement. Then you can pass classes, fields, parameters or methods:
public static String getAnnotationValue(AnnotatedElement element, Class annotationType, String attributeName) {
...
}
I have a complex object that contains two UserPropertyForm objects inside:
public class ComplexUserForm {
int userType;
#Valid
UserPropertyForm property1;
UserPropertyForm property2;
...
}
public class UserPropertyForm {
#NotEmpty
#Length(max = 255)
private String title;
#NotEmpty
#Length(min = 100)
private String description;
...
}
I need property1 be validated every time, so I have marked it as #Valid.
I need property2 be validated only if userType == 2
Could anyone say if I can validate property2 in a simple way using annotations I have for UserPropertyForm fields?
Thanks for any help.
You can use this custom annotation above your class.
#ValidateIfAnotherFieldHasValue(
fieldName = "userType",
fieldValue = "2",
dependFieldName = "property2")
public class ComplexUserForm {
int userType;
#Valid
UserPropertyForm property1;
UserPropertyForm property2;
It will validate property2 only when getUserType().equals("2").
The error messsages will go in property2.fieldname so you'll need
<form:errors path="property2.*"/> in your JSP if you want to catch all errors together from property2.
public class ValidateIfAnotherFieldHasValueValidator
implements ConstraintValidator<ValidateIfAnotherFieldHasValue, Object> {
private String fieldName;
private String expectedFieldValue;
private String dependFieldName;
#Override
public void initialize(final ValidateIfAnotherFieldHasValue annotation) {
fieldName = annotation.fieldName();
expectedFieldValue = annotation.fieldValue();
dependFieldName = annotation.dependFieldName();
}
#Override
public boolean isValid(final Object value, final ConstraintValidatorContext ctx) {
if (value == null) {
return true;
}
try {
final String fieldValue = BeanUtils.getProperty(value, fieldName);
final Object dependFieldValue = PropertyUtils.getProperty(value, dependFieldName);
if (expectedFieldValue.equals(fieldValue)) {
ctx.disableDefaultConstraintViolation();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Object>> errorList = validator.validate(dependFieldValue);
for(ConstraintViolation<Object> error : errorList) {
ctx.buildConstraintViolationWithTemplate(error.getMessageTemplate())
.addNode(dependFieldName+"."+error.getPropertyPath())
.addConstraintViolation();
}
return errorList.isEmpty();
}
} catch (final NoSuchMethodException ex) {
throw new RuntimeException(ex);
} catch (final InvocationTargetException ex) {
throw new RuntimeException(ex);
} catch (final IllegalAccessException ex) {
throw new RuntimeException(ex);
}
return true;
}
}
and:
#Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = ValidateIfAnotherFieldHasValueValidator.class)
#Documented
public #interface ValidateIfAnotherFieldHasValue {
String fieldName();
String fieldValue();
String dependFieldName();
String message() default "{ValidateIfAnotherFieldHasValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
#Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#interface List {
ValidateIfAnotherFieldHasValue[] value();
}
}
I've manage to do that in validate method of form's validator:
public void validate(final Object obj, final Errors errors) {
final ComplexUserForm form = (ComplexUserForm) obj;
if (form.getUserType() == 2) {
ClassValidator<UserPropertyForm> offered2Validator = new ClassValidator<UserPropertyForm>(UserPropertyForm.class);
InvalidValue[] property2InvalidValues = property2Validator.getInvalidValues(form.getProperty2());
for (final InvalidValue invalidValue : property2InvalidValues)
errors.rejectValue("property2." + invalidValue.getPropertyPath(), invalidValue.getMessage(), invalidValue.getMessage());
}
}
}
But I had to add "property2." string to the value's path when rejecting some value of property2 field. If someone knows better way I would be glad to know it. Thanks