package xapi.javac.dev.plugin; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.util.JavacTask; import com.sun.source.util.Plugin; import com.sun.source.util.TaskEvent; import com.sun.source.util.TaskEvent.Kind; import com.sun.source.util.TaskListener; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import xapi.collect.api.IntTo; import xapi.collect.api.StringTo; import xapi.util.impl.ReverseIterable; import xapi.fu.Out2; import xapi.inject.X_Inject; import xapi.javac.dev.api.InjectionResolver; import xapi.javac.dev.api.JavacService; import xapi.javac.dev.api.MagicMethodInjector; import xapi.javac.dev.api.MethodMatcher; import xapi.javac.dev.api.SourceTransformationService; import xapi.javac.dev.search.MagicMethodFinder; import xapi.log.X_Log; import xapi.source.read.JavaModel.IsNamedType; import xapi.util.X_Debug; import static xapi.collect.X_Collect.newList; import static xapi.collect.X_Collect.newStringMap; import java.util.Optional; /** * @author James X. Nelson (james@wetheinter.net) * Created on 3/13/16. */ public class MagicMethodPlugin implements Plugin { private final IntTo<MethodMatcher<MagicMethodInjector>> matchers; private final StringTo<MagicMethodInjector> injectors; private final IntTo<InjectionResolver> changes; private JavacService service; public MagicMethodPlugin() { matchers = newList(MethodMatcher.class); changes = newList(InjectionResolver.class); injectors = newStringMap(MagicMethodInjector.class); } @Override public String getName() { return "MagicMethodPlugin"; } @Override public void init(JavacTask javacTask, String... strings) { service = JavacService.instanceFor(javacTask); service.remember(MagicMethodPlugin.class, this); service.readProperties((key, value)->{ if (key.startsWith("MagicMethod")) { String[] bits = key.split("[|]"); if (bits.length != 2) { X_Log.error(getClass(), "Malformed MagicMethod property; ", "Expected format is: MagicMethod|fully.qualified.ClassName.methodName ", "you may optionally include parameters with java binary types names as well: ", "ex: MagicMethod:java.lang.String.equals(Ljava/lang/Object;)", "You sent: ", key, "=", value); return; } int paramIndex = key.indexOf('('); if (paramIndex == -1) { // match all methods with this name. matchers.add(exe->{ if (!nameMatch(exe, bits[1])) { return Optional.empty(); } return Optional.of(this.createInjector(value)); }); } else { // match only methods with this name and parameters matchers.add(exe->{ if (!nameAndParametersMatch(exe, bits[1])) { return Optional.empty(); } return Optional.of(this.createInjector(value)); }); } } }); javacTask.addTaskListener(new TaskListener() { @Override public void started(TaskEvent taskEvent) { if (taskEvent.getKind() == Kind.ANALYZE) { final CompilationUnitTree cup = taskEvent.getCompilationUnit(); final MagicMethodFinder finder = new MagicMethodFinder(matchers.forEach(), service, cup); finder.visitTopLevel((JCCompilationUnit) cup); final IntTo<Out2<MethodInvocationTree, MagicMethodInjector>> matched = finder.getMatched(); if (matched.isNotEmpty()) { SourceTransformationService editor = SourceTransformationService.instanceFrom(service); matched.readWhileTrue((index, vals) -> { final InjectionResolver resolver = editor.createInjectionResolver(cup); final MethodInvocationTree method = vals.out1(); final MagicMethodInjector injector = vals.out2(); injector.performInjection(cup, method, resolver); if (resolver.hasChanges()) { changes.add(resolver); } return true; }); } } } @Override public void finished(TaskEvent taskEvent) { if (changes.isNotEmpty()) { // We have some source to transform! // For now, we will naively run the transforms in reverse order; // in the future, we will want to be able to apply multiple transformations steps, // such that one transformation which changes another will cause that change to be reprocessed. // In practice, this will likely be done by the magic method resolver, which could, upon finding an injection, // traverse up the scope to find enclosing injections, and mark them as deferred (requiring multiple transforms). for (InjectionResolver injectionResolver : ReverseIterable.reverse(changes.forEach())) { injectionResolver.commit(); } changes.clear(); } } }); } private MagicMethodInjector createInjector(String typeName) { return injectors.getOrCreate(typeName, s->{ Class cls = loadClass(typeName); MagicMethodInjector instance = (MagicMethodInjector) createInstance(cls); instance.init(service); return instance; }); } private Object createInstance(Class cls) { return X_Inject.instance(cls); } private Class loadClass(String typeName) { try { return Class.forName(typeName); } catch (ClassNotFoundException e) { throw X_Debug.rethrow(e); } } private boolean nameAndParametersMatch(IsNamedType exe, String signature) { if (exe.getSimpleName().contentEquals(signature)) { } return false; } private boolean nameMatch(IsNamedType exe, String signature) { return exe.getQualifiedMemberName().equals(signature); } }