I want to decorate existing objects so that method calls are automatically validated. I already managed to delegate method call to an interceptor that calls Hibernate validator and so far it works fine:
public class HibernateBeanValidator implements BeanValidator{
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
#Override
public <T> T addMethodValidation(T object) {
ExecutableValidator executableValidator = factory.getValidator().forExecutables();
Class<? extends T> dynamicType = (Class<? extends T>)new ByteBuddy()
.subclass(object.getClass())
.method(isPublic()).intercept(MethodDelegation.to(new ValidationInterceptor(object, executableValidator)).andThen(SuperMethodCall.INSTANCE))
.make()
.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
try {
T validatedObject = dynamicType.newInstance();
return validatedObject;
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static class ValidationInterceptor {
private final Object validatedObject;
private final ExecutableValidator executableValidator;
public <T> ValidationInterceptor(T object, ExecutableValidator executableValidator) {
this.validatedObject = object;
this.executableValidator = executableValidator;
}
public void validate(#Origin Method method, #AllArguments Object[] arguments)
throws Exception {
Set<ConstraintViolation<Object>> constraintViolations = executableValidator.validateParameters(validatedObject, method, arguments);
if(! constraintViolations.isEmpty()) {
throw new ValidationException(constraintViolations);
}
}
}
}
What I would like to improve is to bind method calls only to methods that have at least one parameter annotated with a constraint annotation, such as:
class Echo {
public String repeat(#NotNull String word) { /* should bind validation here */
return word;
}
public String notAnnotated(String word) { /* should not bind validation */
return word;
}
}
How could I specify an ElementMatcher in Byte Buddy so that it would bind only to methods with parameters annotated with annotations that are annotated with #Constraint, such as #NotNull (taken from javax.validation.constraints):
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = { })
public #interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* Defines several {#link NotNull} annotations on the same element.
*
* #see javax.validation.constraints.NotNull
*/
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
#Retention(RUNTIME)
#Documented
#interface List {
NotNull[] value();
}
}
Your problem can be solved by implementing a custom ElementMatcher which is used to identify methods to be intercepted. Currently, you are using the predefined isPublic() interceptor which does not consider annotations but only the public modifier of a method. As the predefined annotations can be chained, you can build a suitable matcher as follows:
isPublic().and(hasParameter(hasAnnotation(nameStartsWith("javax."))))
Of course, you can simply implement your own matchers without using the predfined ones.
Actually instead of just checking for an annotation out of the javax.validation.constraints namespace, it is probably better to use the Bean Validation meta data API. Constraints do not need to come from this namespace, but can also originate from Hibernate Validator (org.hibernate.validator.constraints) or be a custom constraint. A possible implementation of ElementMatcher which makes use of the meta data API could look like this:
public static class BeanValidationMatcher implements ElementMatcher {
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
#Override
public boolean matches(Object target) {
// handle different descriptors and potentially use generic MethodDescription
if ( !( target instanceof MethodDescription.ForLoadedMethod ) ) {
return false;
}
MethodDescription.ForLoadedMethod methodDescription = (MethodDescription.ForLoadedMethod) target;
Method method = methodDescription.getLoadedMethod();
boolean isGetter = ReflectionHelper.isGetterMethod( method );
boolean needsValidation;
BeanDescriptor beanDescriptor = validator.getConstraintsForClass( method.getDeclaringClass() );
if ( isGetter ) {
needsValidation = isGetterConstrained( method, beanDescriptor );
}
else {
needsValidation = isNonGetterConstrained( method, beanDescriptor );
}
return needsValidation;
}
private boolean isNonGetterConstrained(Method method, BeanDescriptor beanDescriptor) {
return beanDescriptor.getConstraintsForMethod( method.getName(), method.getParameterTypes() ) != null;
}
private boolean isGetterConstrained(Method method, BeanDescriptor beanDescriptor) {
String propertyName = ReflectionHelper.getPropertyName( method );
PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty( propertyName );
return propertyDescriptor != null && propertyDescriptor.findConstraints()
.declaredOn( ElementType.METHOD )
.hasConstraints();
}
}
Related
I'm trying to validate an Enum using the custom validator, in my custom validator I'm trying to return a custom message when a parameter does not exist in the enum values.
Bellow my enum
public enum Type {
MISSING_SITE,
INACTIVE_SITE;
}
Bellow my PostMapping method
#PostMapping(value = "/line-kpi", produces = MediaType.APPLICATION_JSON_VALUE)
#Operation(summary = "Find Kpis by one or more customer property")
public ResponseEntity<List<KpiDTO>> findKPILineByCustomer(#RequestBody #ValidCustomerParameter CustomerParameter customerParameter, #RequestParam #ValidExtractionDate String extractionDate) {
var linesKpi = Optional.ofNullable(
kpiService.findKPILineByCustomer(
Optional.ofNullable(customerParameter.getEntityPerimeter()).orElse(List.of()),
Optional.ofNullable(customerParameter.getName()).orElse(List.of()),
Optional.ofNullable(customerParameter.getIc01()).orElse(List.of()),
Optional.ofNullable(customerParameter.getSiren()).orElse(List.of()),
Optional.ofNullable(customerParameter.getEnterpriseId()).orElse(List.of()),
LocalDate.parse(extractionDate)
)
);
return linesKpi.map(ResponseEntity::ok).orElseThrow(() -> new ResourceNotFoundException(KPIS));
}
I can't switch the type of enum to string in the method itself because I'm using swagger which displays a nice selection list for enums.
Unfortunately, when I try to give a different value for Type, it returns a bad request and my validators are not triggered.
So I'm trying to serialize my enum to be interpreted as String when it arrives at the controller and to do that I need to use Jackson, I tried to look for a solution but I can't find a good one for my case.
Bellow are my validators
public class ReportTypeValidator implements ConstraintValidator<ValidReportType, Type> {
private String globalMessage;
#Override
public void initialize(ValidReportType constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
globalMessage = constraintAnnotation.message();
}
#Override
public boolean isValid(Type type, ConstraintValidatorContext constraintValidatorContext) {
if (Arrays.stream(Type.values()).filter(type1 -> type1.equals(type)).toList().isEmpty()) {
constraintValidatorContext
.buildConstraintViolationWithTemplate(globalMessage + ", report type does not exist")
.addConstraintViolation();
return false;
}
return true;
}
}
#Constraint(validatedBy = ReportTypeValidator.class)
#Target( { ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
#Valid
public #interface ValidReportType {
String message() default "Invalid value for report type";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Could anyone tell me how can I turn my enum as a string so my validator could handle it?
Add a special enum constant indicating the request JSON contained an invalid enum constant name. The request JSON should never actually contain the name of this enum constant. Also add a method that Jackson will invoke while deserializing to convert a JSON string to an enum constant. This method returns the special enum constant if the JSON string is not a known enum constant name.
public enum Type {
MISSING_SITE,
INACTIVE_SITE,
#JsonProperty("SHOULD NEVER ACTUALLY APPEAR IN REQUEST JSON")
INVALID;
/**
* Converts enum constant name to enum constant.
*
* #param name
* enum constant name
* #return enum constant, or {#link #INVALID} if there is no enum constant with that name
*/
#JsonCreator
public static Type valueOfOrInvalid(String name) {
try {
return Type.valueOf(name);
} catch (IllegalArgumentException e) {
return INVALID;
}
}
}
Inside the ReportTypeValidator.isValid( method, check the enum constant is INVALID.
if (type == Type.INVALID) {
// Add constraint violation.
I found it, I was able to do it by implementing a new converter, which will convert string to a valid enum value or an INVALID value:
public class TypeConverter implements Converter<String, Type> {
#Override
public Type convert(String source) {
if (Arrays.stream(Type.values()).filter(type -> Objects.equals(type.toString(), source)).toList().isEmpty()) {
return Type.INVALID;
}
return Type.valueOf(source.toUpperCase());
}
}
After that I added a new Configuration for my converter:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new TypeConverter());
}
}
Also I had to hide the INVALID value of my enum from swagger by adding the #Schema annotation:
#Schema(allowableValues = {"MISSING_SITE","INACTIVE_SITE"}, type = "String")
public enum Type {
MISSING_SITE,
INACTIVE_SITE,
INVALID
}
Finally in the validators, I should reject the INVALID value and display a custom message:
public class ReportTypeValidator implements ConstraintValidator<ValidReportType, Type> {
private String globalMessage;
#Override
public void initialize(ValidReportType constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
globalMessage = constraintAnnotation.message();
}
#Override
public boolean isValid(Type type, ConstraintValidatorContext constraintValidatorContext) {
if (type == Type.INVALID) {
constraintValidatorContext
.buildConstraintViolationWithTemplate(globalMessage)
.addConstraintViolation();
return false;
}
return true;
}
}
The annotation for the previous validator:
#Constraint(validatedBy = ReportTypeValidator.class)
#Target( { ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
#Valid
public #interface ValidReportType {
String message() default "Invalid value for report type";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
My goal is to create an instance from a class that implements an interface and extends another class.
...Entity annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.METHOD, ElementType.FIELD})
public #interface Entity {
String visibileName();
}
...implementsIEventDesignDialog
public class EventDesignDialog implements IEventDesignDialog{
private String show;
private String dateAndTimeDisplayFormat;
private String eventType;
#Entity(visibileName = "Show")
public String getShow() {
return this.show;
}
#Entity(visibileName = "Date And Time display format")
public String getDateAndTimeDisplayFormat() {
return this.dateAndTimeDisplayFormat;
}
#Entity(visibileName = "Event Type")
public String getEventType() {
System.out.println("get event type method invokde successfully");
return this.eventType;
}
}
IEventDesignDialog interface:
public interface IEventDesignDialog extends IPage{
public String getShow();
public String getDateAndTimeDisplayFormat();
public String getEventType();
}
IPage interface:
public interface IPage {
}
Dynamic proxy implementation:
public class IPageProxy implements InvocationHandler {
private List<Method> entityMethods;
private Class <? extends IPage> screenClazz;
public IPageProxy(final Class <? extends IPage> screenClazz) {
entityMethods = new ArrayList<>();
getEntityAnnotatedMethods(screenClazz);
// Accept the real implementation to be proxied
this.screenClazz = screenClazz;
}
/**
* create an page instance
* #param type
* #param
* #return
* #throws InstantiationException
* #throws IllegalAccessException
*/
public static IPage getInstance(final Class<? extends IPage> type)
throws InstantiationException, IllegalAccessException {
List<Class<?>> interfaces = new ArrayList<>();
interfaces.addAll(Arrays.asList(type.getInterfaces()));
return (IPage) Proxy.newProxyInstance(
type.getClassLoader(),
findInterfaces(type),
new IPageProxy(type)
);
/*return (IPage) Proxy.newProxyInstance(type.getClassLoader(),
interfaces.toArray(new Class<?>[interfaces.size()])
, new IPageProxy(type));*/
}
/**
* get all methods that annotated with #Entity annotation
* and add it for entityMethods array List
* #param screenClazz
*/
private void getEntityAnnotatedMethods(final Class <? extends IPage> screenClazz) {
// Scan each interface method for the specific annotation
// and save each compatible method
for (final Method m : screenClazz.getDeclaredMethods()) {
if (m.isAnnotationPresent(Entity.class)) {
entityMethods.add(m);
}
}
}
static Class<?>[] findInterfaces(final Class<? extends IPage> type) {
Class<?> current = type;
do {
final Class<?>[] interfaces = current.getInterfaces();
if (interfaces.length != 0) {
return interfaces;
}
} while ((current = current.getSuperclass()) != Object.class);
throw new UnsupportedOperationException("The type does not implement any interface");
}
#Override
public Object invoke(
final Object proxy,
final Method method,
final Object[] args) throws InvocationTargetException, IllegalAccessException {
// A method on MyInterface has been called!
// Check if we need to go call it directly or if we need to
// execute something else before!
if (entityMethods.contains(method)) {
// The method exist in our to-be-proxied list
// Execute something and the call it
// ... some other things
System.out.println("Something else");
}
// Invoke original method
return method.invoke(screenClazz, args);
}
}
Main class:
public class Main {
public static void main(String[] args) {
try {
((EventDesignDialog)getInstance(EventDesignDialog.class)).getEventType();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
#SuppressWarnings("unchecked")
public static <T extends IPage> T getInstance(final Class<? extends IPage> type) throws InstantiationException, IllegalAccessException {
return (T) IPageProxy.getInstance(type);
}
}
The following exception is thrown:
Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy2 cannot be cast to abc.EventDesignDialog
at abc.Main.main(Main.java:8)
You're extending Screen, which means it isn't an interface.
Dynamic Proxies work only if a base interface is present in the hierarchy.
interfaces.size() == 0
Thus the proxy can't implement any interface, and obviously it isn't part of the Screen hierarchy.
If Screen was an interface, your method is still too complex. This
public static Screen getInstance(Class<? extends Screen> type)
is sufficient.
You still receive an exception because
Class#getInterfaces
returns the interfaces which are implemented by this class.
That means if you invoke it on EventDesignDialog.class, it will return an empty array.
That means if you invoke it on EntityDesignDialog.class, still it will return an empty array.
When invoking it on Screen.class, it will return
[IPage.class]
You need to loop the hierarchy with
Class#getSuperclass
until you find a suitable interface.
A possible implementation might look like
static Class<?>[] findInterfaces(final Class<?> type) {
Class<?> current = type;
do {
final Class<?>[] interfaces = current.getInterfaces();
if (interfaces.length != 0) {
return interfaces;
}
} while ((current = current.getSuperclass()) != Object.class);
throw new UnsupportedOperationException("The type does not implement any interface");
}
which means you need to change your code to
return (IPage) Proxy.newProxyInstance(
type.getClassLoader(),
findInterfaces(type),
new IPageProxy(type)
);
But, being that you already know the result will be an IPage proxy, you can just
return (IPage) Proxy.newProxyInstance(
type.getClassLoader(),
new Class[] { IPage.class },
new IPageProxy(type)
);
Here
public static IPage getInstance(final Class<? extends IPage> type)
you're returning an IPage, but here
((EventDesignDialog)getInstance(EventDesignDialog.class))
you're trying to downcast it, which means you're trying to cast it to a more specific type. This isn't possible as the Proxy isn't of the type EventDesignDialog, but it just implements your IPage interface.
Being that Dynamic Proxies are interface-based, you'll be forced to deal with interfaces.Trying to cast to concrete classes will always throw an exception.
If you need an IEventDesignDialog, you need a new Proxy specifically for it.
I'm exploring annotations and came to a point where some annotations seems to have a hierarchy among them.
I'm using annotations to generate code in the background for Cards. There are different Card types (thus different code and annotations) but there are certain elements that are common among them like a name.
#Target(value = {ElementType.TYPE})
public #interface Move extends Page{
String method1();
String method2();
}
And this would be the common Annotation:
#Target(value = {ElementType.TYPE})
public #interface Page{
String method3();
}
In the example above I would expect Move to inherit method3 but I get a warning saying that extends is not valid with annotations. I was trying to have an Annotation extends a common base one but that doesn't work. Is that even possible or is just a design issue?
You can annotate your annotation with a base annotation instead of inheritance. This is used in Spring framework.
To give an example
#Target(value = {ElementType.ANNOTATION_TYPE})
public #interface Vehicle {
}
#Target(value = {ElementType.TYPE})
#Vehicle
public #interface Car {
}
#Car
class Foo {
}
You can then check if a class is annotated with Vehicle using Spring's AnnotationUtils:
Vehicle vehicleAnnotation = AnnotationUtils.findAnnotation (Foo.class, Vehicle.class);
boolean isAnnotated = vehicleAnnotation != null;
This method is implemented as:
public static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType) {
return findAnnotation(clazz, annotationType, new HashSet<Annotation>());
}
#SuppressWarnings("unchecked")
private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType, Set<Annotation> visited) {
try {
Annotation[] anns = clazz.getDeclaredAnnotations();
for (Annotation ann : anns) {
if (ann.annotationType() == annotationType) {
return (A) ann;
}
}
for (Annotation ann : anns) {
if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {
A annotation = findAnnotation(ann.annotationType(), annotationType, visited);
if (annotation != null) {
return annotation;
}
}
}
}
catch (Exception ex) {
handleIntrospectionFailure(clazz, ex);
return null;
}
for (Class<?> ifc : clazz.getInterfaces()) {
A annotation = findAnnotation(ifc, annotationType, visited);
if (annotation != null) {
return annotation;
}
}
Class<?> superclass = clazz.getSuperclass();
if (superclass == null || Object.class == superclass) {
return null;
}
return findAnnotation(superclass, annotationType, visited);
}
AnnotationUtils also contains additional methods for searching for annotations on methods and other annotated elements. The Spring class is also powerful enough to search through bridged methods, proxies, and other corner-cases, particularly those encountered in Spring.
Unfortunately, no. Apparently it has something to do with programs that read the annotations on a class without loading them all the way. See Why is it not possible to extend annotations in Java?
However, types do inherit the annotations of their superclass if those annotations are #Inherited.
Also, unless you need those methods to interact, you could just stack the annotations on your class:
#Move
#Page
public class myAwesomeClass {}
Is there some reason that wouldn't work for you?
In addition to Grygoriys answer of annotating annotations.
You can check e.g. methods for containing a #Qualifier annotation (or an annotation annotated with #Qualifier) by this loop:
for (Annotation a : method.getAnnotations()) {
if (a.annotationType().isAnnotationPresent(Qualifier.class)) {
System.out.println("found #Qualifier annotation");//found annotation having Qualifier annotation itself
}
}
What you're basically doing, is to get all annotations present on the method and of those annotations you get their types and check those types if they're annotated with #Qualifier. Your annotation needs to be Target.Annotation_type enabled as well to get this working.
Check out https://github.com/blindpirate/annotation-magic , which is a library I developed when I had the same question.
#interface Animal {
boolean fluffy() default false;
String name() default "";
}
#Extends(Animal.class)
#Animal(fluffy = true)
#interface Pet {
String name();
}
#Extends(Pet.class)
#interface Cat {
#AliasFor("name")
String value();
}
#Extends(Pet.class)
#interface Dog {
String name();
}
#interface Rat {
#AliasFor(target = Animal.class, value = "name")
String value();
}
#Cat("Tom")
class MyClass {
#Dog(name = "Spike")
#Rat("Jerry")
public void foo() {
}
}
Pet petAnnotation = AnnotationMagic.getOneAnnotationOnClassOrNull(MyClass.class, Pet.class);
assertEquals("Tom", petAnnotation.name());
assertTrue(AnnotationMagic.instanceOf(petAnnotation, Animal.class));
Animal animalAnnotation = AnnotationMagic.getOneAnnotationOnClassOrNull(MyClass.class, Animal.class);
assertTrue(animalAnnotation.fluffy());
Method fooMethod = MyClass.class.getMethod("foo");
List<Animal> animalAnnotations = AnnotationMagic.getAnnotationsOnMethod(fooMethod, Animal.class);
assertEquals(Arrays.asList("Spike", "Jerry"), animalAnnotations.stream().map(Animal::name).collect(toList()));
I have implemented an enum validator following the older posts here. I was wondering whether this following code is thread safe or not? I have got many different enums for which I need to use this validator. Is this going to create any problems?
#Documented
#Constraint(validatedBy = StringEnumerationValidator.class)
#Target({ElementType.FIELD,ElementType.ANNOTATION_TYPE,
ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#NotNull(message="Value must not be null.")
public #interface StringEnumeration {
String message() default "{com.xxx.bean.validation.constraints.StringEnumeration.message}";
Class<?>[] groups() default{};
Class<? extends Payload>[] payload() default{};
Class<? extends Enum<?>> enumClass();
}
public class StringEnumerationValidator implements
ConstraintValidator<StringEnumeration, String> {
private Set<String> AVAILABLE_ENUM_NAMES;
public static Set<String> getNamesSet(Class<? extends Enum<?>> e){
Enum<?>[] enums = e.getEnumConstants();
String[] names = new String[enums.length];
for (int i = 0; i < enums.length; i++) {
names[i] = enums[i].name();
System.out.println(enums.length);
System.out.println(enums[i]);
}
Set<String> mySet = new HashSet<String>(Arrays.asList(names));
return mySet;
}
#Override
public void initialize(StringEnumeration stringEnumeration) {
Class<? extends Enum<?>> enumSelected = stringEnumeration.enumClass();
AVAILABLE_ENUM_NAMES = getNamesSet(enumSelected);
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
else {
return AVAILABLE_ENUM_NAMES.contains(value);
}
}
EDIT: Sources referred:
JSR-303 Bean Validation annotate multiple fields
How to use Hibernate validation annotations with enums?
Java String validation using enum values and annotation
Yes and no. Strictly speaking, it is not thread safe, because your initialize method could be called concurrently by two different threads. This method writes to the AVAILABLE_ENUM_NAMES variable, and thus the two threads might interfer with each other.
But if you make sure that initialize is called only once for each instance, before other threads have access to that instance, it is thread safe because isValid only reads the set and does not modify it.
To ensure that initializing is done only once, you should do it in the constructor:
public StringEnumeratorValidator(StringEnumeration stringEnumeration) {
Class<? extends Enum<?>> enumSelected = stringEnumeration.enumClass();
AVAILABLE_ENUM_NAMES = getNamesSet(enumSelected);
}
and remove the intialize method.
I'm developing a Java enterprise application, currently doing Java EE security stuff to restrict access for particular functions to specific users. I configured the application server and everything, and now I'm using the RolesAllowed-annotation to secure the methods:
#Documented
#Retention (RUNTIME)
#Target({TYPE, METHOD})
public #interface RolesAllowed {
String[] value();
}
When I use the annotation like this, it works fine:
#RolesAllowed("STUDENT")
public void update(User p) { ... }
But this is not what I want, as I have to use a String here, refactoring becomes hard, and typos can happen. So instead of using a String, I would like to use an Enum value as a parameter for this annotation. The Enum looks like this:
public enum RoleType {
STUDENT("STUDENT"),
TEACHER("TEACHER"),
DEANERY("DEANERY");
private final String label;
private RoleType(String label) {
this.label = label;
}
public String toString() {
return this.label;
}
}
So I tried to use the Enum as a parameter like this:
#RolesAllowed(RoleType.DEANERY.name())
public void update(User p) { ... }
But then I get the following compiler error, although Enum.name just returns a String (which is always constant, isn't it?).
The value for annotation attribute RolesAllowed.value must be a constant expression`
The next thing I tried was to add an additional final String to my Enum:
public enum RoleType {
...
public static final String STUDENT_ROLE = STUDENT.toString();
...
}
But this also doesn't work as a parameter, resulting in the same compiler error:
// The value for annotation attribute RolesAllowed.value must be a constant expression
#RolesAllowed(RoleType.STUDENT_ROLE)
How can I achieve the behavior I want? I even implemented my own interceptor to use my own annotations, which is beautiful, but far too much lines of code for a little problem like this.
DISCLAIMER
This question was originally a Scala question. I found out that Scala is not the source of the problem, so I first try to do this in Java.
How about this?
public enum RoleType {
STUDENT(Names.STUDENT),
TEACHER(Names.TEACHER),
DEANERY(Names.DEANERY);
public class Names{
public static final String STUDENT = "Student";
public static final String TEACHER = "Teacher";
public static final String DEANERY = "Deanery";
}
private final String label;
private RoleType(String label) {
this.label = label;
}
public String toString() {
return this.label;
}
}
And in annotation you can use it like
#RolesAllowed(RoleType.Names.DEANERY)
public void update(User p) { ... }
One little concern is, for any modification, we need to change in two places. But since they are in same file, its quite unlikely to be missed. In return, we are getting the benefit of not using raw strings and avoiding the sophisticated mechanism.
Or this sounds totally stupid? :)
I don't think your approach of using enums is going to work. I found that the compiler error went away if I changed the STUDENT_ROLE field in your final example to a constant string, as opposed to an expression:
public enum RoleType {
...
public static final String STUDENT_ROLE = "STUDENT";
...
}
However, this then means that the enum values wouldn't be used anywhere, because you'd be using the string constants in annotations instead.
It seems to me that you'd be better off if your RoleType class contained nothing more than a bunch of static final String constants.
To see why your code wasn't compiling, I had a look into the Java Language Specification (JLS). The JLS for annotations states that for an annotation with a parameter of type T and value V,
if T is a primitive type or String, V is a constant expression.
A constant expression includes, amongst other things,
Qualified names of the form TypeName . Identifier that refer to constant variables
and a constant variable is defined as
a variable, of primitive type or type String, that is final and initialized with a compile-time constant expression
Here's a solution using an additional interface and a meta-annotation. I've included a utility class to help do the reflection to get the role types from a set of annotations, and a little test for it:
/**
* empty interface which must be implemented by enums participating in
* annotations of "type" #RolesAllowed.
*/
public interface RoleType {
public String toString();
}
/** meta annotation to be applied to annotations that have enum values implementing RoleType.
* the value() method should return an array of objects assignable to RoleType*.
*/
#Retention(RetentionPolicy.RUNTIME)
#Target({ANNOTATION_TYPE})
public #interface RolesAllowed {
/* deliberately empty */
}
#RolesAllowed
#Retention(RetentionPolicy.RUNTIME)
#Target({TYPE, METHOD})
public #interface AcademicRolesAllowed {
public AcademicRoleType[] value();
}
public enum AcademicRoleType implements RoleType {
STUDENT, TEACHER, DEANERY;
#Override
public String toString() {
return name();
}
}
public class RolesAllowedUtil {
/** get the array of allowed RoleTypes for a given class **/
public static List<RoleType> getRoleTypesAllowedFromAnnotations(
Annotation[] annotations) {
List<RoleType> roleTypesAllowed = new ArrayList<RoleType>();
for (Annotation annotation : annotations) {
if (annotation.annotationType().isAnnotationPresent(
RolesAllowed.class)) {
RoleType[] roleTypes = getRoleTypesFromAnnotation(annotation);
if (roleTypes != null)
for (RoleType roleType : roleTypes)
roleTypesAllowed.add(roleType);
}
}
return roleTypesAllowed;
}
public static RoleType[] getRoleTypesFromAnnotation(Annotation annotation) {
Method[] methods = annotation.annotationType().getMethods();
for (Method method : methods) {
String name = method.getName();
Class<?> returnType = method.getReturnType();
Class<?> componentType = returnType.getComponentType();
if (name.equals("value") && returnType.isArray()
&& RoleType.class.isAssignableFrom(componentType)) {
RoleType[] features;
try {
features = (RoleType[]) (method.invoke(annotation,
new Object[] {}));
} catch (Exception e) {
throw new RuntimeException(
"Error executing value() method in "
+ annotation.getClass().getCanonicalName(),
e);
}
return features;
}
}
throw new RuntimeException(
"No value() method returning a RoleType[] type "
+ "was found in annotation "
+ annotation.getClass().getCanonicalName());
}
}
public class RoleTypeTest {
#AcademicRolesAllowed({DEANERY})
public class DeaneryDemo {
}
#Test
public void testDeanery() {
List<RoleType> roleTypes = RolesAllowedUtil.getRoleTypesAllowedFromAnnotations(DeaneryDemo.class.getAnnotations());
assertEquals(1, roleTypes.size());
}
}
I solved this by using Lombok annotation FieldNameConstants :
#FieldNameConstants(onlyExplicitlyIncluded = true)
public enum EnumBasedRole {
#FieldNameConstants.Include ADMIN,
#FieldNameConstants.Include EDITOR,
#FieldNameConstants.Include READER;
}
Next you can use it as follow :
#RestController
#RequestMapping("admin")
#RolesAllowed(EnumBasedRole.Fields.ADMIN)
public class MySecuredController {
#PostMapping("user")
public void deleteUser(...) {
...
}
}
I solved this problem by adding an annotation #RoleTypesAllowed and adding a metadata source. This works really well if there is just one enum type that needs to be supported. For multiple enum types, see anomolos's post.
In the below RoleType is my role enum.
#Documented
#Target({ElementType.TYPE, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface RoleTypesAllowed {
RoleType[] value();
}
Then I added the following metadata source to spring...
#Slf4j
public class CemsRolesAllowedMethodSecurityMetadataSource
extends AbstractFallbackMethodSecurityMetadataSource {
protected Collection<ConfigAttribute> findAttributes(Class<?> clazz) {
return this.processAnnotations(clazz.getAnnotations());
}
protected Collection<ConfigAttribute> findAttributes(Method method, Class<?> targetClass) {
return this.processAnnotations(AnnotationUtils.getAnnotations(method));
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
private List<ConfigAttribute> processAnnotations(Annotation[] annotations) {
if (annotations != null && annotations.length != 0) {
List<ConfigAttribute> attributes = new ArrayList();
for (Annotation a : annotations) {
if (a instanceof RoleTypesAllowed) {
RoleTypesAllowed ra = (RoleTypesAllowed) a;
RoleType[] alloweds = ra.value();
for (RoleType allowed : alloweds) {
String defaultedAllowed = new RoleTypeGrantedAuthority(allowed).getAuthority();
log.trace("Added role attribute: {}", defaultedAllowed);
attributes.add(new SecurityConfig(defaultedAllowed));
}
return attributes;
}
}
}
return null;
}
}