Zaxxon
Zaxxon

Reputation: 182

Is there a way to make Java Annotation required if another is used?

I have not used java.lang.annotation much at all, but for the simplest way of metadata. I was wondering if there is a way to setup within an annotation or a group of annotations such that if conditional A is meet, then annotation A.B or B needs to be present.

For example

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Independent {
  public boolean isDependent() default false;
  public String[] dependency() default "";
}

So if isDependent is set to true then the developer/user would need to set dependency with some values (ideally, what the dependency(-ies) is).

Is this possible to capture this meta-data here? If so, how to enforce it?

Restriction: Project has it's own "framework", so cannot import Spring Framework, which had some @Required annonations.

Did see this for Conditionally required property using data annotations but it's for C# not Java.

Thank you.

Upvotes: 0

Views: 1223

Answers (1)

murtiko
murtiko

Reputation: 273

You can create "meta-annotations" to annotate your annotations.

For example this annotation can be used on an annotation to list the dependent annotations it requires.

package com.acme;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * If your annotation requires other annotations to be present as well,
 * you can list them in your annotation declaration using this annotation.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface RequiresAnnotation {
  String[] annotations();
}

A sample processor for this @RequiresAnnotation meta-annotation:

    @Processor("com.acme.RequiresAnnotation")
    void processRequiresAnnotation(Target target, Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        RequiresAnnotation requiresAnnotation = target.getAnnotation()
                .<RequiresAnnotation>getAnnotation(RequiresAnnotation.class);
        List<String> requiredAnnotations = Arrays.asList(requiresAnnotation.annotations());
        for (Element element : target.getElements()) {
            debug(target.getAnnotation().getQualifiedName() + " -> " + RequiresAnnotation.class.getName()
                    + " is found on [" + element.getSimpleName() + "]");
            List<? extends AnnotationMirror> annos = element.getAnnotationMirrors();
            List<String> foundAnnos = new LinkedList<String>();
            for (AnnotationMirror anno : annos) {
                String name = ((TypeElement) anno.getAnnotationType().asElement()).getQualifiedName().toString();
                debug("found [" + name + "] annotation in [" + element + "]");
                foundAnnos.add(name);
            }
            for (String required : requiredAnnotations) {
                if (!foundAnnos.contains(required))
                    throw new RuntimeException("[" + ((TypeElement) element).getQualifiedName()
                            + "] must be annotated with [" + required + "]");
            }
        }
    }

...

    void debug(Object message) {
        this.processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message.toString());
    }

To use it, assume you have a MyAnnotation that requires another annotation called MyOtherAnnotation.

package com.acme;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import com.acme.RequiresAnnotation;

@Documented
@Retention(RUNTIME)
@Target(TYPE)
@RequiresAnnotation(annotations = {"com.acme.MyOtherAnnotation" })
public @interface MyAnnotation {

}
package com.acme;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;


@Documented
@Retention(RUNTIME)
@Target(TYPE)
public @interface MyOtherAnnotation {

}

Now if you annotate a class with MyAnnotation, you will have to add MyOtherAnnotation as well:

package com.acme;

@MyAnnotation
@MyOtherAnnotation
public class MyClass {
    ...
}

If you don't, you will get compilation error:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project annotations-tests: Fatal error compiling: java.lang.RuntimeException: java.lang.reflect.InvocationTargetException: [com.acme.MyClass] must be annotated with [com.acme.MyOtherAnnotation] -> [Help 1]

I have extracted the above info from a small meta-annotations library that i had written some time ago, but it is not available publicly unfortunately. I am sure there are some other libraries out there as well though..

Upvotes: 1

Related Questions