package xapi.javac.dev.impl;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.expr.NameExpr;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskEvent.Kind;
import com.sun.source.util.TaskListener;
import com.sun.tools.javac.api.BasicJavacTask;
import com.sun.tools.javac.api.MultiTaskListener;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Name;
import xapi.annotation.inject.SingletonDefault;
import xapi.collect.X_Collect;
import xapi.collect.api.StringTo;
import xapi.fu.In1;
import xapi.fu.Out2;
import xapi.fu.Rethrowable;
import xapi.javac.dev.api.CompilerService;
import xapi.javac.dev.api.JavacService;
import xapi.javac.dev.api.SourceTransformationService;
import xapi.javac.dev.model.CompilationUnitTaskList;
import xapi.javac.dev.model.CompilerSettings;
import xapi.javac.dev.model.InjectionBinding;
import xapi.javac.dev.model.JavaDocument;
import xapi.javac.dev.search.InjectionTargetSearchVisitor;
import xapi.log.X_Log;
import xapi.source.X_Source;
import xapi.util.X_Debug;
import static xapi.fu.In2.ignoreFirst;
import javax.lang.model.element.TypeElement;
import javax.tools.FileObject;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import java.io.File;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* @author James X. Nelson (james@wetheinter.net)
* Created on 4/3/16.
*/
@SingletonDefault(implFor = CompilerService.class)
public class CompilerServiceImpl implements CompilerService, Rethrowable {
private JavacService service;
Map<String, CompilationUnitTaskList> cups = new HashMap<>();
SortedMap<String, JavaDocument> documentsByType = new TreeMap<>();
StringTo<StringTo<JavaDocument>> documentsInUnit = X_Collect.newStringDeepMap(JavaDocument.class);
Set<CompilationUnitTaskList> unfinished = new HashSet<>();
List<Runnable> onFinish = new ArrayList<>();
List<In1<JavaDocument>> listeners = new ArrayList<>();
boolean finished;
@Override
public void init(JavacService service) {
this.service = service;
}
@Override
public Out2<Integer, URL> compileFiles(CompilerSettings settings, String ... files) {
int result = com.sun.tools.javac.Main.compile(settings.toArguments(files), new PrintWriter(System.out));
File f = new File(settings.getOutputDirectory());
try {
return Out2.out2Immutable(result, f.toURI().toURL());
} catch (MalformedURLException e) {
throw rethrow (e);
}
}
@Override
public void record(CompilationUnitTree cup, TypeElement typeElement) {
getOrMakeDocument(cup, typeElement);
final List<InjectionBinding> bindings = new InjectionTargetSearchVisitor(service, cup)
.scan(cup, new ArrayList<>());
}
private JavaDocument getOrMakeDocument(CompilationUnitTree cup, TypeElement typeElement) {
String typeName = typeElement.getQualifiedName().toString();
JavaDocument doc = documentsByType.get(typeName);
doc = new JavaDocument(doc, service, cup, typeElement);
documentsByType.put(typeName, doc);
String cupName = doc.getCompilationUnitName();
documentsInUnit.get(cupName).put(typeName, doc);
if (cups.containsKey(cupName)) {
final CompilationUnitTaskList existing = cups.get(cupName);
existing.setUnit(cup);
} else {
cups.put(cupName, new CompilationUnitTaskList(cupName, cup));
}
CompilationUnitTaskList pcu = cups.get(cupName);
pcu.onFinished(In1.ignored(()->
documentsInUnit.get(cupName)
// forBoth accepts an In2 type, but we only want to act on the second type, which is a JavaDocument,
// so we use In2.ignoreFirst to call the finish() method on each document
.forBoth(ignoreFirst(JavaDocument::finish))
));
return doc;
}
@Override
public void peekOnCompiledUnits(In1<JavaDocument> listener) {
listeners.add(listener);
documentsByType.values().forEach(listener.toConsumer());
}
@Override
public void onCompilationUnitFinished(String name, In1<CompilationUnitTree> callback) {
CompilationUnitTaskList pcu = cups.get(name);
if (pcu == null) {
pcu = new CompilationUnitTaskList(name, null);
cups.put(name, pcu);
finished = false;
}
pcu.onFinished(callback);
}
@Override
public void onFinished(Runnable r) {
if (finished) {
r.run();
} else {
onFinish.add(r);
}
}
protected void finish(CompilationUnitTree cup, BasicJavacTask task) {
String name = service.getQualifiedName(cup);
final CompilationUnitTaskList pcu = cups.get(name);
pcu.finish();
boolean almostDone = unfinished.size() == 1;
unfinished.remove(pcu);
if (almostDone && unfinished.isEmpty()) {
// all compilation units have been parsed. Lets clear out all the pending units
clearPendingUnits(task);
}
}
protected void clearPendingUnits(BasicJavacTask task) {
List<CompilationUnitTaskList> missing = new ArrayList<>();
cups.values()
.stream()
.filter(pcu -> pcu.getUnit() == null)
.forEach(missing::add);
if (!missing.isEmpty()) {
Context ctx = new Context(task.getContext());
ctx.put(JavacService.class, service);
MultiTaskListener tasks = MultiTaskListener.instance(ctx);
tasks.add(getTaskListener(task));
JavacProcessingEnvironment env = JavacProcessingEnvironment.instance(ctx);
try (
JavacFileManager jfm = new JavacFileManager(ctx, false, Charset.forName("UTF-8"))
) {
List<JavaFileObject> missingFiles = new ArrayList<>();
missing.forEach(pcu -> {
String name = pcu.getName();
TypeElement element = task.getElements().getTypeElement(name);
if (element == null) {
X_Log.info(getClass(), "No element found for ", name);
} else {
final javax.lang.model.element.Name qualified = element.getQualifiedName();
Name binary = env.getElementUtils().getBinaryName(element);
String relativeName = binary.toString().split("[$]")[0];// TODO check for enclosing types instead of this nasty hack
JavaFileObject javaFile;
try {
javaFile = jfm.getJavaFileForInput(StandardLocation.SOURCE_OUTPUT, relativeName, JavaFileObject.Kind.SOURCE);
if (javaFile == null) {
javaFile = jfm.getJavaFileForInput(StandardLocation.SOURCE_PATH, relativeName, JavaFileObject.Kind.SOURCE);
if (javaFile == null) {
javaFile = jfm.getJavaFileForInput(StandardLocation.CLASS_PATH, relativeName, JavaFileObject.Kind.SOURCE);
if (javaFile == null) {
X_Log.warn(getClass(), "Unable to find file ", relativeName, " to recompile...");
}
}
}
if (javaFile != null){
missingFiles.add(javaFile);
}
} catch (Throwable e) {
X_Log.warn(getClass(), "Unable to compile source for "+binary, e);
throw X_Debug.rethrow(e);
}
// pcu.setUnit(cu);
// pcu.finish();
}
});
JavaCompiler compiler = JavaCompiler.instance(ctx);
final com.sun.tools.javac.util.List<JavaFileObject> javaFiles = com.sun.tools.javac.util.List.from(missingFiles.toArray(new JavaFileObject[missingFiles.size()]));
compiler.compile(javaFiles);
} catch (Throwable throwable) {
throw X_Debug.rethrow(throwable);
}
}
doFinish();
}
@Override
public void overwriteCompilationUnit(JavaDocument doc, String newSource) {
String pkg = doc.getPackageName();
String fileName = doc.getFileName();
final CompilationUnit parsed = doc.getAst();
final String originalSource = doc.getSource();
final PackageDeclaration packageDcl = parsed.getPackage();
String currentPackage = packageDcl.getName().getName();
final String distPackage = outputPackage();
final String tmpPackage = workingPackage();
final NameExpr parsedName = parsed.getPackage().getName();
if (currentPackage.startsWith(tmpPackage)) {
// already running in tmp. check if this file has been finalized or not.
parsedName.setName(parsedName.getName().replace(tmpPackage+".", distPackage+"."));
} else if (currentPackage.startsWith(distPackage)) {
// document has been finalized.
// we may want to add listeners who only want to see a document after it has stabilized
doc.finalize(service);
} else {
// move the file into /tmp package and recompile until it becomes stable.
parsedName.setName(X_Source.qualifiedName(tmpPackage, parsedName.getName()));
}
if (!parsedName.getName().equals(pkg)) {
// mutate all packages starting with the original package.
// TODO: store up references to all original packages with a manager who can, at any time,
// find all the import statements belonging to a given prefix,
parsed.getImports().stream()
.filter(importDecl -> importDecl.getName().getName().startsWith(pkg))
.forEach(importDecl -> {
String name = importDecl.getName().getName();
name = name.replace(pkg, parsedName.getName());
final NameExpr nameExpr = new NameExpr(name);
importDecl.setName(nameExpr);
});
SourceTransformationService sources = service.getSourceTransformService();
sources.recordRepackage(doc, pkg, parsedName.getName());
}
String finalSource = parsed.toSource(service.getTransformer());
if (!originalSource.equals(finalSource)) {
// TODO add Generated annotation which includes hashes of original files, and steps along the ways
final JavaFileManager filer = service.getFileManager();
try {
final FileObject output = filer.getJavaFileForOutput(
StandardLocation.SOURCE_OUTPUT,
parsedName.getName() + "." + fileName,
JavaFileObject.Kind.SOURCE,
doc.getSourceFile()
);
try (
Writer writer = output.openWriter()
) {
writer.append(finalSource);
}
if (isGreedyCompiler()) {
onCompilationUnitFinished(doc.getAstName(), In1.noop());
}
} catch (Throwable e) {
throw rethrow(e);
}
}
}
@Override
public boolean isGreedyCompiler() {
return true;
}
@Override
public JavaDocument getDocument(CompilationUnitTree cup) {
final String type = service.getQualifiedName(cup);
return documentsByType.computeIfAbsent(type, ignored->
getOrMakeDocument(cup, service.getElements().getTypeElement(type))
);
}
protected void doFinish() {
finished = true;
final List<Runnable> copy;
synchronized (onFinish) {
copy = new ArrayList<>(onFinish);
onFinish.clear();
}
copy.forEach(Runnable::run);
copy.clear();
}
@Override
public TaskListener getTaskListener(JavacTask task) {
BasicJavacTask javacTask = (BasicJavacTask) task;
return new TaskListener() {
@Override
public void started(TaskEvent e) {
javacTask.getContext().put(JavacService.class, service);
if (e.getKind() == Kind.ANALYZE) {
record(e.getCompilationUnit(), e.getTypeElement());
}
}
@Override
public void finished(TaskEvent e) {
if (e.getKind() == Kind.ANALYZE) {
finish(e.getCompilationUnit(), javacTask);
clearPendingUnits(javacTask);
}
}
};
}
}