Search code examples
javaenumsannotationsannotation-processing

Is it possible to get a hold of values associated with an enum from inside an annotation processor?


In the case below, I want to get the value of MyString.class since the @RendererType is of value TEXT on the field inside the annotation processor's execution.

This is my custom annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Renderer {

  RendererType value() default RendererType.TEXT;
}

which contains a RendererType enum whos definition is:

public enum RendererType {
  TEXT(new Class[] {MyString.class}),
  TEXTAREA(new Class[] {MyString.class}),
  CHECKBOX(new Class[] {MyBoolean.class});

  private final Class<?>[] supportedTypes;

  RendererType(Class<?>[] classes) {
    this.supportedTypes = classes;
  }

  public Class<?>[] getSupportedTypes() {
    return this.supportedTypes;
  }
}

Example usage of @Renderer on a field in a POJO would look like:

@Renderer(RendererType.CHECKBOX)
boolean old = true;

This is the annotation processor StringProcessor, for @Renderer annotation:

import com.company.annotation.metadata.Renderer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;

public class StringProcessor extends AbstractProcessor {

  private Messager messager;

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    messager = processingEnv.getMessager();
  }

  @Override
  public boolean process(
      final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {

    for (TypeElement typeElement : annotations) {
        for (Element element : roundEnv.getElementsAnnotatedWith(typeElement)) {

            TypeMirror typeMirror = element.asType();

            ElementKind elementKind = element.getKind();

            if (elementKind.equals(ElementKind.FIELD)) {
                System.out.println("FIELD");
                // Do something to get `supportedTypes` associated with the enum value
            }
        }
    }

    return false;
  }

  @Override
  public Set<String> getSupportedAnnotationTypes() {
    final Set<String> annotations = new HashSet<>(Arrays.asList(Renderer.class.getName()));
    return annotations;
  }
}

Solution

  • If your annotation processor has access to Renderer at compile-time, which I assume it does given your implementation of getSupportedAnnotationTypes, then the easiest approach is to use a class literal with AnnotatedConstruct#getAnnotation(Class). That will give you an instance of Renderer which you can then use like normal.

    Example

    Renderer.java
    package com.example;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Renderer {
    
      RendererType value() default RendererType.TEXT;
    }
    
    RendererType.java
    package com.example;
    
    import java.util.List;
    
    public enum RendererType {
      TEXT(String.class),
      TEXTAREA(String.class),
      CHECKBOX(Boolean.class, boolean.class);
    
      // using unmodifiable list because lists are generally better than
      // arrays and you no longer have to worry about returning a defensive
      // copy to avoid globally-scoped modifications
      private final List<Class<?>> supportedTypes;
    
      // vararg parameter makes declaring the constants a little cleaner
      RendererType(Class<?>... supportedTypes) {
        // List::of returns an unmodifiable list
        this.supportedTypes = List.of(supportedTypes);
      }
    
      public List<Class<?>> getSupportedTypes() {
        return supportedTypes;
      }
    }
    

    Note: If you still want to use Class<?>[] then make sure that getSupportedTypes() returns a copy of the array.

    RendererProcessor.java
    package com.example.processing;
    
    import com.example.Renderer;
    import java.util.Set;
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.RoundEnvironment;
    import javax.annotation.processing.SupportedAnnotationTypes;
    import javax.annotation.processing.SupportedSourceVersion;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.TypeElement;
    
    // replace source version as needed
    @SupportedSourceVersion(SourceVersion.RELEASE_21)
    // or override 'getSupportedAnnotationTypes'
    @SupportedAnnotationTypes("com.example.Renderer")
    public class RendererProcessor extends AbstractProcessor {
    
      @Override
      public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Renderer.class)) {
          // no need to verify element is field due to @Target meta-annotation
          Renderer renderer = element.getAnnotation(Renderer.class);
          List<Class<?>> supportedTypes = renderer.value().getSupportedTypes();
          // TODO: do something with 'supportedTypes'
        }
        return false;
      }
    }
    

    Note: If you override getSupportedAnnotationTypes then return Set.of(Renderer.class.getName()) can be the implementation.