I am writing an AnnotationProcessor which is supposed to generate java code. It should generate a derived interface from certain existing interfaces.
For this purpose I need to find the import statements of the original input code, so that I can output it in the generated java file.
How can this be done?
You can't get import statements with an annotation processor. What you can get though, are the types used by that class, which is even better.
Import statements from the source code are not enough for analyzing what types are used in a class, because not all used types have import statements. If you really only need the actual statements, you could read the source file directly.
There are some issues if you only look at the statements:
java.util.Date date;
With the annotation processor and the Mirror API, you can get the types of properties, method parameters, method return types, etc. - basically the types of every declaration that is not in a method or block. This should be good enough.
You should analyse every element of the class and store it's type in a Set. There are some utility classes that help with this task. You can ignore any type in the java.lang
package since it is always implicitly imported.
A minimal annotation processor may look like this:
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.TypeElement;
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("*")
public class Processor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
ImportScanner scanner = new ImportScanner();
scanner.scan(roundEnv.getRootElements(), null);
Set<String> importedTypes = scanner.getImportedTypes();
// do something with the types
return false;
}
}
The Scanner here extends ElementScanner7
which is based on a visitor pattern. We only implement a few visitor methods and filter elements by kind because not all elements can actually contain importable types.
import java.util.HashSet;
import java.util.Set;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementScanner7;
public class ImportScanner extends ElementScanner7<Void, Void> {
private Set<String> types = new HashSet<>();
public Set<String> getImportedTypes() {
return types;
}
@Override
public Void visitType(TypeElement e, Void p) {
for(TypeMirror interfaceType : e.getInterfaces()) {
types.add(interfaceType.toString());
}
types.add(e.getSuperclass().toString());
return super.visitType(e, p);
}
@Override
public Void visitExecutable(ExecutableElement e, Void p) {
if(e.getReturnType().getKind() == TypeKind.DECLARED) {
types.add(e.getReturnType().toString());
}
return super.visitExecutable(e, p);
}
@Override
public Void visitTypeParameter(TypeParameterElement e, Void p) {
if(e.asType().getKind() == TypeKind.DECLARED) {
types.add(e.asType().toString());
}
return super.visitTypeParameter(e, p);
}
@Override
public Void visitVariable(VariableElement e, Void p) {
if(e.asType().getKind() == TypeKind.DECLARED) {
types.add(e.asType().toString());
}
return super.visitVariable(e, p);
}
}
This scanner returns a set of types as fully qualified paths. There are still a few things to consider and some things to implement:
java.lang
and also types from the same packagejava.util.List<String>
TypeKind.DECLARED
is not the only kind of elements that is an importable type. Also check TypeKind.ARRAY
and get the actual declared type of it. Instead of adding another to else if(e.asType().getKind() == TypeKind.ARRAY) // ...
the class TypeKindVisitor7
could be used insteadElementScanner6
, TypeKindVisitor6
etc. implementations.It looks like there is no way to get import statements from the standard SDK classes (at least with SDK 5-6-7).
Nevertheless, you can use some classes inside tools.jar from SUN/Oracle.
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
public class MyProcessor extends AbstractProcessor {
@Override
public void init(ProcessingEnvironment env) {
tree = Trees.instance(env);
}
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnvironment) {
for( Element rootElement : roundEnvironment.getRootElements() ) {
TreePath path = tree.getPath(rootElement);
System.out.println( "root element "+rootElement.toString() +" "+path.getCompilationUnit().getImports().size() );
}
....
To get the jar of Java tools via maven, refer to this thread.
There should be an alternative using a TreePathScanner (from tools.jar as well) but the visitImport method was never triggered for me.
<dependency>
<groupId>org.jvnet.sorcerer</groupId>
<artifactId>sorcerer-javac</artifactId>
<version>0.8</version>
</dependency>
Using this dependency fix the problem, but unfortunately this time it supports up to Java 1.7 and you cannot correctly compile Java 1.8 source. My solution is a little bit hack, but it works without using this dependency and with Java 1.8 sources
public final class SorcererJavacUtils {
private static final Pattern IMPORT = Pattern.compile("import\\s+(?<path>[\\w\\\\.]+\\*?)\\s*;");
// com.sun.tools.javac.model.JavacElements
public static Set<String> getImports(Element element, ProcessingEnvironment processingEnv) {
Elements elements = processingEnv.getElementUtils();
Class<?> cls = elements.getClass();
try {
Method getTreeAndTopLevel = cls.getDeclaredMethod("getTreeAndTopLevel", Element.class);
getTreeAndTopLevel.setAccessible(true);
// Pair<JCTree, JCCompilationUnit>
Object treeTop = getTreeAndTopLevel.invoke(elements, element);
if (treeTop == null)
return Collections.emptySet();
// JCCompilationUnit
Object toplevel = getFieldValue("snd", treeTop);
return SorcererJavacUtils.<List<Object>>getFieldValue("defs", toplevel).stream()
.map(Object::toString)
.map(IMPORT::matcher)
.filter(Matcher::find)
.map(matcher -> matcher.group("path"))
.collect(Collectors.toSet());
} catch(Exception ignored) {
return Collections.emptySet();
}
}
private static <T> T getFieldValue(String name, Object obj) throws IllegalAccessException, NoSuchFieldException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
return (T)field.get(obj);
}
private SorcererJavacUtils() {
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With