package xapi.dev.gwtc.impl;
import org.junit.Test;
import xapi.annotation.compile.Dependency;
import xapi.annotation.compile.ResourceBuilder;
import xapi.annotation.inject.InstanceDefault;
import xapi.dev.gwtc.api.GwtcService;
import xapi.dev.source.ClassBuffer;
import xapi.dev.source.MethodBuffer;
import xapi.file.X_File;
import xapi.gwtc.api.GwtManifest;
import xapi.gwtc.api.Gwtc;
import xapi.gwtc.api.GwtcProperties;
import xapi.io.api.SimpleLineReader;
import xapi.log.X_Log;
import xapi.shell.X_Shell;
import xapi.shell.api.ShellSession;
import xapi.test.junit.JUnit4Runner;
import xapi.test.junit.JUnitUi;
import xapi.util.X_Debug;
import xapi.util.X_Properties;
import xapi.util.X_Util;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.RunAsyncCallback;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.dev.Compiler;
import com.google.gwt.dev.GwtCompiler;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.junit.tools.GWTTestSuite;
import com.google.gwt.reflect.shared.GwtReflect;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Gwtc(propertiesLaunch=@GwtcProperties)
@InstanceDefault(implFor=GwtcService.class)
public class GwtcServiceImpl extends GwtcServiceAbstract {
private String binDir;
Pattern SPECIAL_DIRS = Pattern.compile(
"("+Pattern.quote(Dependency.DIR_BIN)+")|"+
"("+Pattern.quote(Dependency.DIR_GEN)+")|"+
"("+Pattern.quote(Dependency.DIR_TEMP)+")|"
);
private MethodBuffer junitLoader;
public GwtcServiceImpl() {
this(Thread.currentThread().getContextClassLoader());
}
public GwtcServiceImpl(ClassLoader resourceLoader) {
super(resourceLoader);
}
@Override
public void addAsyncBlock(Class<? extends RunAsyncCallback> asSubclass) {
}
@Override
public void addGwtTestCase(Class<? extends GWTTestCase> subclass) {
}
@Override
public void addGwtTestSuite(Class<? extends GWTTestSuite> asSubclass) {
}
@Override
public void addMethod(Method method) {
addMethod(method, false);
}
@Override
public void addMethod(Method method, boolean onNewInstance) {
if (Modifier.isStatic(method.getModifiers())){
// print a call to a static method
out.println(out.formatStaticCall(method));
} else {
// print a call to an instance method; creating an instance if necessary.
out.println(out.formatInstanceCall(method, onNewInstance));
}
}
@Override
public void addPackage(Package pkg, boolean recursive) {
if (!finished.add(pkg.getName())) {
return;
}
Gwtc gwtc = pkg.getAnnotation(Gwtc.class);
context.addPackages(pkg, this, recursive);
if (gwtc != null) {
context.addGwtcPackage(gwtc, pkg, recursive);
}
}
@Override
public int compile(GwtManifest manifest) {
String gwtHome = generateCompile(manifest);
// Logging
X_Log.info(getClass(), "Starting gwt compile", manifest.getModuleName());
X_Log.trace(manifest);
final String[] programArgs = manifest.toProgramArgArray(false);
final String[] jvmArgs = manifest.toJvmArgArray();
X_Log.trace("Args: java ", jvmArgs, programArgs);
final String[] classpath = manifest.toClasspathFullCompile(getTempDir().getAbsolutePath(), gwtHome);
X_Log.debug("Requested Classpath\n", classpath);
final int[] result = new int[]{-1};
if (manifest.isUseCurrentJvm()) {
assert runtimeContainsClasspath(manifest.getGenDir(), classpath);
// TODO launch a worker thread for us to block on...
// Preferably using a brandnew URLClassLoader
URL[] urls = new URL[classpath.length];
for (int i = 0; i < classpath.length; i++) {
try {
urls[i] = new URL("file:" + classpath[i]);
} catch (MalformedURLException e) {
X_Log.error(getClass(), "Bad url: ", classpath[i]);
throw X_Util.rethrow(e);
}
}
System.out.println(Arrays.asList(urls));
// Explicitly do not give it a parent classloader;
// we want to isolate the compilation's classpath to whatever was supplied in the manifest.
// URLClassLoader loader = new URLClassLoader(urls, null);
URLClassLoader loader = new URLClassLoader(urls, Thread.currentThread().getContextClassLoader()) {
@Override
public URL getResource(String name) {
URL url = super.getResource(name);
if (url == null) {
// Egregious hack; when running in the same jvm, the classloader is not able to use
// the bootstrap classpath to load our newly generated resources out of our generated folder,
// so, we resort to manually looking up any failed resource loads here, in the classloader.
try {
File f = new File(inGeneratedDirectory(manifest, name));
if (f.exists()) {
return f.toURI().toURL();
}
} catch (IOException e) {
X_Log.warn(getClass(), "Failure to load resources for " + name);
}
}
return url;
}
};
Thread t = new Thread(()->{
final boolean success = GwtCompiler.doCompile(programArgs);
result[0] = success ? 0 : 1;
});
t.setContextClassLoader(loader);
t.start();
try {
t.join(60_000);
} catch (InterruptedException e) {
throw X_Debug.rethrow(e);
}
} else {
ShellSession controller
= X_Shell.launchJava(GwtCompiler.class,
classpath,
jvmArgs,
programArgs
);
controller.stdErr(new SimpleLineReader() {
@Override
public void onLine(String errLog) {
warn("[ERROR] "+errLog);
}
});
controller.stdOut(new SimpleLineReader() {
@Override
public void onLine(String logLine) {
info(logLine);
}
});
result[0] = controller.block(60, TimeUnit.SECONDS);
}
if (result[0] != 0) {
error("Gwt compile for "+manifest.getModuleName()+" finished w/ non-successful exit code "+
result);
}
X_Log.info("Entry point: "+new File(manifest.getWarDir(), context.getGenName()+".html"));
switch (manifest.getCleanupMode()) {
case DELETE_ON_SUCCESSFUL_EXIT:
if (result[0] != 0) {
return result[0];
}
case DELETE_ON_EXIT:
Runtime.getRuntime().addShutdownHook(new Thread(()->{
X_File.deepDelete(manifest.getWarDir());
X_File.deepDelete(manifest.getWorkDir());
}));
break;
case DELETE_ON_SUCCESS:
if (result[0] != 0) {
return result[0];
}
case ALWAYS_DELETE:
X_File.deepDelete(manifest.getWarDir());
X_File.deepDelete(manifest.getWorkDir());
}
return result[0];
}
private boolean runtimeContainsClasspath(String genDir, String[] classpath) {
final URL[] runtimeCp = ((URLClassLoader) getClass().getClassLoader()).getURLs();
X_Log.debug("Runtime cp", runtimeCp);
searching:
for (URL url : runtimeCp) {
for (String s : classpath) {
if (genDir.equals(s)) {
continue searching;
}
if (url.toExternalForm().endsWith(s)) {
continue searching;
}
}
return false;
}
return true;
}
public String generateCompile(GwtManifest manifest) {
assert tempDir.exists() : "No usable directory "+tempDir.getAbsolutePath();
X_Log.info(getClass(), "Generated entry point", "\n"+getEntryPoint());
X_Log.info(getClass(), "Generated module", "\n"+getGwtXml(manifest));
if (manifest.getModuleName() == null) {
manifest.setModuleName(genName);
manifestName = genName;
} else {
manifestName = manifest.getModuleName();
context.setRenameTo(manifest.getModuleName());
}
saveGwtXmlFile(context.getGwtXml(manifest), manifest.getModuleName(), manifest);
manifest.getModules().forEach(mod->{
saveGwtXmlFile(mod.getBuffer(), mod.getInheritName(), manifest);
});
String entryPointLocation = inGeneratedDirectory(manifest, entryPoint.getQualifiedName().replace('.', '/')+".java");
saveTempFile(entryPoint.toString(), new File(entryPointLocation));
files.forBoth((path, body)->
saveTempFile(body.out1(), new File(inGeneratedDirectory(manifest, path)))
);
return prepareCompile(manifest);
}
@Override
public String inGeneratedDirectory(GwtManifest manifest, String filename) {
String genFolder = manifest.getGenDir();
try {
return new File(genFolder, filename).getCanonicalPath();
} catch (IOException e) {
throw X_Debug.rethrow(e);
}
}
private String replaceLocationVars(GwtManifest manifest, String value) {
Matcher matcher = SPECIAL_DIRS.matcher(value);
List<Replacement> replacements = new ArrayList<Replacement>();
if (matcher.matches()) {
String type = matcher.group();
switch (type) {
case Dependency.DIR_BIN:
if (binDir == null) {
binDir = X_Properties.getProperty("java.class.path", "bin");
}
replacements.add(new Replacement(matcher.start(), matcher.end(), binDir));
break;
case Dependency.DIR_GEN:
replacements.add(new Replacement(matcher.start(), matcher.end(), manifest.getGenDir()));
break;
case Dependency.DIR_TEMP:
replacements.add(new Replacement(matcher.start(), matcher.end(), tempDir.getAbsolutePath()));
break;
}
}
for (int i = replacements.size(); i-->0;) {
Replacement replacement = replacements.get(i);
value = value.substring(0, replacement.start)
+ replacement.newValue+value.substring(replacement.end);
}
return value;
}
private String resolveDependency(GwtManifest manifest, Dependency dependency) {
switch (dependency.dependencyType()) {
case ABSOLUTE:
return replaceLocationVars(manifest, dependency.value());
case RELATIVE:
if (dependency.groupId().isEmpty()) {
return replaceLocationVars(manifest, dependency.value());
} else {
return replaceLocationVars(manifest, dependency.groupId())+
File.separator+
replaceLocationVars(manifest, dependency.version());
}
case MAVEN:
String m2Home = X_Properties.getProperty("maven.home");
if (m2Home == null) {
m2Home = X_Properties.getProperty("user.home");
if (m2Home != null) {
File f = new File(m2Home,".m2/repository");
if (f.exists()) {
m2Home = f.getAbsolutePath();
}
}
if (m2Home == null) {
X_Log.warn(getClass(), "Cannot resolve maven dependency",dependency
,"as M2_HOME environment variable is not set");
}
}
if (m2Home != null) {
File artifact = new File(m2Home, dependency.groupId().replace('.', File.separatorChar));
artifact = new File(artifact, dependency.value());
artifact = new File(artifact, dependency.version());
if (dependency.classifier().length() > 0) {
artifact = new File(artifact, dependency.value()+"-"+dependency.version()+"-"+dependency.classifier()+".jar");
} else {
artifact = new File(artifact, dependency.value()+"-"+dependency.version()+".jar");
}
if (artifact.exists()) {
X_Log.trace(getClass(), "Using maven artifact ",artifact.getAbsolutePath());
return artifact.getAbsolutePath();
} else {
X_Log.warn(getClass(),"could not find maven dependency",dependency,"in",artifact);
}
}
}
return null;
}
protected GwtcProperties getDefaultLaunchProperties() {
return getClass().getAnnotation(Gwtc.class).propertiesLaunch()[0];
}
protected String prepareCompile(GwtManifest manifest) {
GwtcProperties defaultProp = getDefaultLaunchProperties();
Type level = manifest.getLogLevel();
for (GwtcProperties prop : context.getLaunchProperties()) {
if (prop.obfuscationLevel() != defaultProp.obfuscationLevel()) {
manifest.setObfuscationLevel(prop.obfuscationLevel());
}
if (prop.logLevel() != defaultProp.logLevel()) {
if (level.isLowerPriorityThan(prop.logLevel())) {
level = prop.logLevel();
}
}
}
manifest.setLogLevel(level);
manifest.addSystemProp("xapi.log.level="+level.name());
if (manifest.getWarDir() == null) {
File f = tempDir;
try {
manifest.setWarDir(f.getCanonicalPath());
X_Log.info(getClass(), "Manifest WAR: ",manifest.getWarDir());
} catch (IOException e) {
X_Log.warn("Unable to create temporary war directory for GWT compile",
"You will likely get an unwanted war folder in the directory you executed this program");
X_Debug.maybeRethrow(e);
}
}
if (manifest.getUnitCacheDir() == null) {
try {
File f = X_File.createTempDir("gwtc-"+manifest.getModuleName()+"UnitCache", manifest.isDisableUnitCache());
if (f != null) {
manifest.setUnitCacheDir(f.getCanonicalPath());
}
} catch (IOException e) {
X_Log.warn("Unable to create unit cache work directory for GWT compile",
"You will likely get unwanted gwtUnitcache folders in the directory you executed this program");
}
}
for (Dependency dependency : context.getDependencies()) {
manifest.addDependency(resolveDependency(manifest, dependency));
}
String gwtHome = X_Properties.getProperty("gwt.home");
if (gwtHome == null) {
URL gwtHomeLocation = Compiler.class.getClassLoader().getResource(Compiler.class.getName().replace('.', '/')+".class");
if (gwtHomeLocation == null) {
X_Log.warn("Unable to find gwt home from System property gwt.home, "
, "nor from looking up the gwt compiler class from classloader. Defaulting to ./lib");
gwtHome = X_File.getPath(".");
} else {
gwtHome = gwtHomeLocation.toExternalForm();
if (gwtHome.contains("jar!")) {
gwtHome = gwtHome.split("jar!")[0]+"jar";
}
gwtHome = gwtHome.replace("file:", "").replace("jar:", "");
if (manifest.getGwtVersion().length() == 0) {
if (gwtHome.contains("gwt-dev.jar")) {
manifest.setGwtVersion("");
} else {
manifest.setGwtVersion(extractGwtVersion(gwtHome));
}
}
int ind = gwtHome.lastIndexOf("gwt-dev");
gwtHome = gwtHome.substring(0, ind-1);
}
}
generateWar(manifest);
return gwtHome;
}
@Override
public boolean addJUnitClass(Class<?> clazz) {
if (!finished.add(clazz.getName())) {
X_Log.info(getClass(), "Skipped JUnit 4 class",clazz);
return false;
}
search: {
for (Method m : clazz.getMethods()) {
if (m.isAnnotationPresent(Test.class)) {
break search;
}
}
return false;
}
Gwtc gwtc = clazz.getAnnotation(Gwtc.class);
X_Log.info(getClass(), "generating JUnit class", clazz, "?"+(gwtc != null));
if (gwtc != null) {
context.addGwtcClass(gwtc, clazz);
}
addGwtModules(clazz);
X_Log.info(getClass(), "added test class for JUnit 4",clazz);
ensureReportError();
inheritGwtXml(clazz, ResourceBuilder.buildResource("org.junit.JUnit4").build());
inheritGwtXml(clazz, ResourceBuilder.buildResource("com.google.gwt.core.Core").build());
ClassBuffer cb = classBuffer();
String simple = cb.addImport(clazz);
String methodName = "add"+simple+"Tests";
String gwt = cb.addImport(GWT.class);
String callback = cb.addImport(RunAsyncCallback.class);
String magic = cb.addImportStatic(GwtReflect.class, "magicClass");
cb.createMethod("void "+methodName)
.println(gwt+".runAsync("+simple+".class,")
.indent()
.println("new "+callback+ "() {")
.indent()
.println("public void onSuccess() {")
.indent()
.println(magic+"("+simple+".class);")
.startTry()
.println("junit.addTests("+simple+".class);")
.startCatch("Throwable", "e")
.println("junit.print(\"Error adding "+simple+" to unit test\", e);")
.endTry()
.outdent()
.println("}")
.println()
.println("public void onFailure(Throwable reason) {")
.indent()
.println("junit.print(\"Error loading "+simple+"\", reason);")
.outdent()
.println("}")
.outdent()
.println("}")
.outdent()
.println(");")
;
junitLoader.println(methodName+"();");
return true;
}
@Override
protected void generateReportError(ClassBuffer classBuffer) {
super.generateReportError(classBuffer);
addClass(JUnit4Runner.class);
junitLoader = classBuffer.createInnerClass("private final class JUnit extends JUnitUi")
.createMethod("public void loadAllTests()");
classBuffer.createField(JUnitUi.class, "junit")
.setModifier(Modifier.FINAL | Modifier.PRIVATE)
.setInitializer("new JUnit()");
out.println("junit.onModuleLoad();");
}
}