package xapi.javac.dev.impl; import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.ImportTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.source.util.Trees; import com.sun.tools.javac.api.JavacScope; import com.sun.tools.javac.api.JavacTrees; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.file.JavacFileManager; import com.sun.tools.javac.main.JavaCompiler; import com.sun.tools.javac.model.JavacElements; import com.sun.tools.javac.model.JavacTypes; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.tree.JCTree.JCFieldAccess; import com.sun.tools.javac.tree.JCTree.JCIdent; import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; import com.sun.tools.javac.tree.JCTree.JCVariableDecl; import com.sun.tools.javac.tree.TreeInfo; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.Name; import xapi.annotation.inject.InstanceDefault; import xapi.collect.X_Collect; import xapi.collect.api.ClassTo; import xapi.collect.api.StringDictionary; import xapi.collect.api.StringTo; import xapi.fu.In2; import xapi.fu.Out2; import xapi.javac.dev.api.JavacService; import xapi.javac.dev.model.InjectionBinding; import xapi.javac.dev.model.InjectionMap; import xapi.javac.dev.model.XApiInjectionConfiguration; import xapi.log.X_Log; import xapi.source.X_Source; import xapi.source.read.JavaModel.IsNamedType; import xapi.source.read.JavaModel.IsType; import javax.annotation.processing.Filer; import javax.lang.model.element.TypeElement; import javax.lang.model.type.MirroredTypeException; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.JavaFileManager; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.charset.Charset; import java.util.Enumeration; import java.util.NoSuchElementException; import java.util.Optional; import java.util.Properties; import java.util.Set; /** * @author James X. Nelson (james@wetheinter.net) * Created on 3/12/16. */ @InstanceDefault(implFor = JavacService.class) public class JavacServiceImpl implements JavacService { private Types types; private Elements elements; private InjectionMap injections; private JavacTrees trees; private JavaCompiler compiler; private StringDictionary<String> props; private final ClassTo cache; private final StringTo<Out2<TypeElement, CompilationUnitTree>> typeMap; private JavaFileManager filer; private Filer procFiler; public JavacServiceImpl() { cache = X_Collect.newClassMap(); typeMap = X_Collect.newStringMap(Out2.class); injections = new InjectionMap(); remember(InjectionMap.class, injections); remember(JavacService.class, this); props = X_Collect.newDictionary(); } @Override @SuppressWarnings("unchecked") public <T, C extends Class<T>> T recall(C cls) { return (T) cache.get(cls); } @Override @SuppressWarnings("unchecked") public <T, C extends Class<T>> T remember(C cls, T value) { return (T) cache.put(cls, value); } @Override public String getPackageName(CompilationUnitTree cu) { if (cu instanceof JCCompilationUnit) { JCCompilationUnit compilationUnit = (JCCompilationUnit) cu; return compilationUnit.packge.toString(); } throw new UnsupportedOperationException("Cannot handle compilation unit type " + cu.getClass()+" of compilation unit " + cu); } @Override public String getFileName(CompilationUnitTree cu) { return getClassTree(cu).getSimpleName().toString(); } @Override public TypeMirror findType(ExpressionTree init) { switch (init.getKind()) { case MEMBER_SELECT: MemberSelectTree member = (MemberSelectTree) init; assert member instanceof JCFieldAccess : "Member "+member+" must be a JCFieldAccess"; JCFieldAccess asField = (JCFieldAccess)member; if (member.getIdentifier().contentEquals("class")) { // must be a field access if (asField.type != null && getTypeName(asField).contentEquals("java.lang.Class")) { // Grab the type parameter off the class literal List<Type> params = asField.type.allparams(); if (params.last() != null) { return params.last(); } } // Grab the type parameter off the class literal final TypeElement ele = elements.getTypeElement(asField.getExpression().toString()); assert ele.asType() != null; return ele.asType(); } if (asField.type != null) { CharSequence name = asField.type.tsym.getQualifiedName(); final TypeElement ele = elements.getTypeElement(name); assert ele != null; return ele.asType(); } throw new UnsupportedOperationException("Could not find type of " + init); case IDENTIFIER: IdentifierTree ident = (IdentifierTree) init; if (ident instanceof JCIdent) { JCIdent asIdent = (JCIdent) ident; assert asIdent.type != null; return extractClassType(asIdent.type) .orElseGet(()->{ final TypeElement ele = elements.getTypeElement(asIdent.sym.flatName().toString()); assert ele.asType() != null; return ele.asType(); }); } throw new UnsupportedOperationException("Could not find type of identifier " + init+"; unhandled type " + init.getKind()); case METHOD_INVOCATION: MethodInvocationTree invoke = (MethodInvocationTree) init; if (invoke instanceof JCMethodInvocation) { JCMethodInvocation asMethod = (JCMethodInvocation) invoke; assert asMethod.type != null; if (getTypeName(asMethod).contentEquals("java.lang.Class")) { List<Type> params = asMethod.type.allparams(); if (params.last() != null) { return params.last(); } throw new UnsupportedOperationException("Cannot handle raw class type in " + asMethod); } X_Log.warn(getClass(), "Selecting type", asMethod.getMethodSelect().type, "from", asMethod); return asMethod.getMethodSelect().type; } throw new UnsupportedOperationException("Could not find type of method invocation " + init+"; unhandled type " + init.getKind()); } throw new UnsupportedOperationException("Could not find type of " + init+"; unhandled type " + init.getKind()); } private Optional<TypeMirror> extractClassType(Type type) { if (type.asElement().flatName().contentEquals("java.lang.Class")) { List<Type> params = type.allparams(); if (params.last() != null) { return Optional.of(params.last()); } return Optional.empty(); } return Optional.empty(); } private Name getTypeName(JCMethodInvocation asMethod) { return asMethod.type.asElement().flatName(); } private Name getTypeName(JCFieldAccess asField) { return asField.type.asElement().flatName(); } @Override public ClassTree getClassTree(CompilationUnitTree cu) { String name = new File(cu.getSourceFile().getName()).getName().replace(".java", ""); if (cu instanceof JCCompilationUnit) { JCCompilationUnit unit = (JCCompilationUnit) cu; return unit.getTypeDecls().stream() .filter(decl->decl instanceof ClassTree) .map(decl->(ClassTree)decl) .filter(decl->decl.getSimpleName().contentEquals(name)) .findFirst() .get(); } return null; } @Override public String getQualifiedName(CompilationUnitTree cup, ClassTree classTree) { String pkg = getPackageName(cup); if ("unnamed package".equals(pkg)) { return classTree.getSimpleName().toString(); } // TODO consider enclosing elements correctly... return X_Source.qualifiedName(pkg, classTree.getSimpleName().toString()); } @Override public String getQualifiedName(CompilationUnitTree cup, Tree tree) { if (tree instanceof CompilationUnitTree) { return getQualifiedName((CompilationUnitTree)tree); } else if (tree instanceof ImportTree) { ImportTree importTree = (ImportTree) tree; return getQualifiedName(cup, importTree.getQualifiedIdentifier()); } else if (tree instanceof ClassTree) { return getQualifiedName(cup, (ClassTree)tree); } else if (tree instanceof ExpressionTree){ final TypeMirror type = findType((ExpressionTree) tree); return type.toString(); } else { X_Log.error(getClass(), "Unhandled tree subclass ", tree.getKind(), " with class ", tree.getClass()); throw new UnsupportedOperationException("Not able to determine qualified name of " + tree); } } @Override public Optional<InjectionBinding> getInjectionBinding(XApiInjectionConfiguration config, TypeMirror type) { String scope; try { scope = config.getSettings().scope().getName(); } catch (MirroredTypeException e) { scope = e.getTypeMirror().toString(); } String typeName = types.erasure(type).toString(); if ("Test".equals(typeName)) { InjectionBinding egregiousHack = new InjectionBinding("Test", "ComplexTest"); return Optional.of(egregiousHack); } return injections.getBinding(scope, typeName); } @Override public void init(Context context) { types = JavacTypes.instance(context); elements = JavacElements.instance(context); trees = JavacTrees.instance(context); compiler = JavaCompiler.instance(context); filer = context.get(JavaFileManager.class); if (filer == null) { filer = new JavacFileManager(context, true, Charset.forName("UTF-8")); } Filer proc = context.get(Filer.class); if (proc == null) { proc = procFiler; } if (proc != null) { procFiler = proc; remember(Filer.class, proc); } remember(JavaFileManager.class, filer); remember(Types.class, types); remember(Elements.class, elements); remember(Trees.class, trees); remember(JavaCompiler.class, compiler); try { final Enumeration<URL> propFiles = Thread.currentThread().getContextClassLoader().getResources( "META-INF/xapi.properties"); while (propFiles.hasMoreElements()) { final URL location = propFiles.nextElement(); final Properties properties = new Properties(); properties.load(location.openStream()); if (properties.isEmpty()) { continue; } final Set<String> names = properties.stringPropertyNames(); for (String name : names) { final String value = (String) properties.get(name); final Object result = props.setValue(name, value); if (result != null) { if (!result.equals(value)) { X_Log.warn(getClass(), "Overwriting property ", name, " was ", result, " set to: ", value); } } } } } catch (IOException e) { e.printStackTrace(); } } @Override public InjectionBinding createInjectionBinding(VariableTree node) { return new InjectionBinding(this, node); } @Override public InjectionBinding createInjectionBinding(MethodTree node) { return new InjectionBinding(this, node); } @Override public IsType getInvocationTargetType(CompilationUnitTree cup, MethodInvocationTree node) { if (node instanceof JCMethodInvocation) { final TreePath path = trees.getPath(cup, node); final TreePath parent = path.getParentPath(); final Tree target = parent.getLeaf(); switch (target.getKind()) { case VARIABLE: JCVariableDecl var = (JCVariableDecl) target; final JCTree type = var.getType(); final JavacScope scope = trees.getScope(path); final IsType cls = getTypeOf(cup, scope, type); return cls; default: X_Log.error(getClass(), "Unhandled invocation target type: ", target.getKind(), target); } } else { X_Log.warn(getClass(), "Does not support MethodInvocationTree ", node); } return null; } private IsType getTypeOf(CompilationUnitTree cup, JavacScope scope, JCTree type) { switch (type.getKind()) { case IDENTIFIER: JCIdent ident = (JCIdent) type; for (ImportTree importTree : cup.getImports()) { if (importTree.isStatic()) { continue; // TODO: handle differently... (write a test!) } final Tree id = importTree.getQualifiedIdentifier(); final String name = TreeInfo.fullName((JCTree) id).toString(); if (name.endsWith(ident.getName().toString())) { // we have a winner! return new IsType(name.toString()); } if ("*".equals(name)) { } } final String pkg = getPackageName(cup); return new IsType(pkg, ident.getName().toString()); } return null; } @Override public IsNamedType getName(CompilationUnitTree cup, MethodInvocationTree node) { if (node instanceof JCMethodInvocation) { final String simpleName = TreeInfo.name(((JCMethodInvocation) node).meth).toString(); String fullName = TreeInfo.fullName(((JCMethodInvocation) node).meth).toString(); String className = fullName.indexOf('.') == -1 ? fullName : fullName.replaceFirst("[.](?:[^.]+)$", ""); if (fullName.equals(className)) { // This is a local method reference. Find the enclosing class type ClassTree cls = getEnclosingClass(cup, node); className = getNameOf(cls); return IsNamedType.namedType(className, simpleName); } final TreePath path = trees.getPath(cup, node); final JavacScope scope = trees.getScope(path); if (scope.isStarImportScope()) { // need to do lookup on the simple name of the method } else { // need to do lookup on the class of the imported method } final Optional<? extends ImportTree> match = cup.getImports().stream() .filter(importName -> { final Name n = TreeInfo.name((JCTree) importName.getQualifiedIdentifier()); return n.contentEquals(simpleName); }) .findFirst(); return IsNamedType.namedType(className, simpleName); } else { X_Log.warn(getClass(), "Does not support MethodInvocationTree ", node); } return null; } public String getNameOf(ClassTree cls) { if (cls instanceof JCClassDecl) { return ((JCClassDecl)cls).sym.getQualifiedName().toString(); } throw new NoSuchElementException("Cannot find name of " + cls); } @Override public ClassTree getEnclosingClass(CompilationUnitTree cup, Tree node) { TreePath path = trees.getPath(cup, node); while (!(path.getLeaf() instanceof ClassTree) && path.getParentPath() != null) { path = path.getParentPath(); } if (path.getLeaf() instanceof ClassTree) { return (ClassTree) path.getLeaf(); } throw new NoSuchElementException("Cannot find a parent class of " + node); } @Override public void readProperties(In2<String, String> in) { props.forEach(in); } }