package xapi.javac.dev.impl;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.expr.NameExpr;
import com.sun.source.tree.CompilationUnitTree;
import xapi.annotation.inject.InstanceDefault;
import xapi.collect.X_Collect;
import xapi.collect.api.IntTo;
import xapi.collect.api.StringTo;
import xapi.fu.Rethrowable;
import xapi.inject.X_Inject;
import xapi.javac.dev.api.CompilerService;
import xapi.javac.dev.api.InjectionResolver;
import xapi.javac.dev.api.JavacService;
import xapi.javac.dev.api.SourceTransformationService;
import xapi.javac.dev.model.JavaDocument;
import xapi.javac.dev.model.SourceRange;
import xapi.javac.dev.model.SourceTransformation;
import xapi.javac.dev.model.SourceTransformation.SourceTransformType;
import javax.tools.JavaFileObject;
import java.util.Arrays;
import java.util.Optional;
/**
* @author James X. Nelson (james@wetheinter.net)
* Created on 4/3/16.
*/
@InstanceDefault(implFor = SourceTransformationService.class)
public class SourceTransformationServiceImpl implements SourceTransformationService, Rethrowable {
private JavacService service;
private CompilerService compiler;
private StringTo.Many<SourceTransformation> transforms = X_Collect.newStringMultiMap(SourceTransformation.class);
@Override
public void init(JavacService service) {
this.service = service;
compiler = CompilerService.compileServiceFrom(service);
}
@Override
public InjectionResolver createInjectionResolver(CompilationUnitTree cup) {
InjectionResolver resolver = X_Inject.instance(InjectionResolver.class);
resolver.init(service, cup, this);
return resolver;
}
@Override
public void requestOverwrite(CompilationUnitTree cup, int startPos, int endPos, String newSource) {
final JavaDocument doc = compiler.getDocument(cup);
final String name = doc.getTypeName();
final IntTo<SourceTransformation> jobs = getJobs(doc, name);
jobs.add(new SourceTransformation()
.setCompilationUnit(cup)
.setTransformType(SourceTransformType.REPLACE)
.setRange(new SourceRange(startPos, endPos))
// TODO add expected text
.setText(newSource)
);
}
private IntTo<SourceTransformation> getJobs(JavaDocument doc, String name) {
return getJobs(doc, name, false);
}
private IntTo<SourceTransformation> getJobs(JavaDocument doc, String name, boolean forceAdd) {
final IntTo<SourceTransformation> jobs = transforms.get(name);
if (forceAdd || jobs.isEmpty()) {
compiler.onCompilationUnitFinished(name, c->{
final SourceTransformation[] items = jobs.toArray();
if (items.length == 0) {
// no transforms? Lets finalize the document if it is in the working directory.
finalizeDocument(doc);
} else {
jobs.clear();
String newSource = applyTransformations(name, doc, items);
compiler.overwriteCompilationUnit(doc, newSource);
}
});
};
return jobs;
}
private void finalizeDocument(JavaDocument doc) {
if (doc.getPackageName().startsWith(compiler.workingPackage())) {
String newPackage = doc.getPackageName().replace(compiler.workingPackage() + "." , compiler.outputPackage() + ".");
if (!newPackage.equals(doc.getPackageName())) {
String newSource = applyTransformations(doc.getTypeName(), doc, repackage(doc, doc.getPackageName(),
newPackage));
compiler.overwriteCompilationUnit(doc, newSource);
}
}
}
private String applyTransformations(String name, JavaDocument doc, SourceTransformation ... jobs) {
final JavaFileObject sourceFile = doc.getSourceFile();
String source = doc.getSource();
if (jobs.length > 0) {
Arrays.sort(jobs, (a, b)->a.getRange().compareTo(b.getRange()));
// Grab the expected source so we don't accidentally overwrite an index that has moved.
for (SourceTransformation job : jobs) {
job.setExpected(job.getRange().slice(source));
}
// To avoid corrupting indexes, we will apply ranges backwards.
for (int i = jobs.length; i-->0; ) {
final SourceTransformation job = jobs[i];
final Optional<String> transformed = applyTransform(doc, job);
if (transformed.isPresent()) {
source = transformed.get();
} else {
// If the transform was not applied, we will put the job back on the queue.
// Also, let the job know that it failed so that it can re-perform its modification on the expected source.
// We let it see the sources we have when it failed so that it can, if it is able, just refind a particular range.
// even if that position is moved again, this job will eventually stabilize, if it is able to recover.
job.setFailedTransform(source, name, sourceFile);
getJobs(doc, name).add(job);
}
}
}
return source;
}
private Optional<String> applyTransform(JavaDocument doc, SourceTransformation job) {
final SourceRange range = job.getRange();
final String source = doc.getSource();
String current = range.slice(source);
if (!current.equals(job.getExpected())) {
return Optional.empty();
}
StringBuilder b = new StringBuilder();
switch (job.getTransformType()) {
case REMOVE:
if (range.getStart() > 0) {
b.append(source.substring(0, range.getStart()));
}
if (range.getEnd() < source.length()) {
b.append(source.substring(range.getEnd()));
}
break;
case REPLACE:
if (range.getStart() > 0) {
b.append(source.substring(0, range.getStart()));
}
b.append(job.getText());
if (range.getEnd() < source.length()) {
b.append(source.substring(range.getEnd()));
}
break;
case REPACKAGE:
String newPackage = job.getText();
String oldPackage = job.getExtraText();
if ("".equals(newPackage) || doc.getAst().getPackage() == null) {
doc.getAst().setPackage(new PackageDeclaration(new NameExpr(newPackage)));
} else {
doc.getAst().getPackage().setName(new NameExpr(oldPackage));
}
// fallthrough; we also want to do a change import during a repackage.
case CHANGE_IMPORT:
String newPkg = job.getText();
String oldPkg = job.getExtraText();
final CompilationUnit ast = doc.getAst();
ast.getImports()
.stream()
.forEach(importDecl -> {
String name = importDecl.getName().getName();
String repackaged = name.replace(oldPkg, newPkg);
if ((!importDecl.isAsterisk() && name.equals(oldPkg)) ||
name.startsWith(doc.getTypeName())) {
if (ast.getImports().stream().noneMatch(
imported -> imported.getName().getName().equals(repackaged))) {
importDecl.getName().setName(repackaged);
}
} else if (importDecl.isAsterisk()) {
if (name.equals(oldPkg)) {
assert !importDecl.isStatic();
// If there was an asterisk import on the old package, add a new import for the repackaged type,
// but check first that this import does not exist.
if (ast.getImports().stream().noneMatch(
imported -> imported.isAsterisk() && imported.getName().getName().equals(newPkg))) {
ast.getImports().add(new ImportDeclaration(new NameExpr(newPkg), false, true));
}
}
}
});
return Optional.of(doc.getAst().toSource(service.getTransformer()));
case WRAP:
if (range.getStart() > 0) {
b.append(source.substring(0, range.getStart()));
}
if (job.getText() != null) {
b.append(job.getText());
}
b.append(current);
if (job.getExtraText() != null) {
b.append(job.getExtraText());
}
if (range.getEnd() < source.length()) {
b.append(source.substring(range.getEnd()));
}
break;
}
return Optional.of(b.toString());
}
@Override
public void recordRepackage(JavaDocument doc, String oldPackage, String newPackage) {
getJobs(doc, doc.getTypeName(), true)
.add(repackage(doc, oldPackage, newPackage)
);
// TODO make sure compiler service cleans these listeners up (or throw away compiler service on each compile)...
// or maintain a set of remappings we've done to be able to reuse.
compiler.peekOnCompiledUnits(otherDoc->{
if (otherDoc.hasImport(oldPackage)) {
getJobs(otherDoc, otherDoc.getTypeName(), true)
.add(changeImport(doc, oldPackage, newPackage));
}
});
}
private SourceTransformation repackage(JavaDocument doc, String oldPackage, String newPackage) {
return new SourceTransformation()
.setCompilationUnit(doc.getCompilationUnit())
.setText(newPackage)
.setExtraText(oldPackage)
.setTransformType(SourceTransformType.REPACKAGE);
}
private SourceTransformation changeImport(JavaDocument doc, String oldPackage, String newPackage) {
return new SourceTransformation()
.setCompilationUnit(doc.getCompilationUnit())
.setText(newPackage)
.setExtraText(oldPackage)
.setTransformType(SourceTransformType.CHANGE_IMPORT);
}
}