xapi.dev.ui.autoui.AutoUiGenerator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xapi-dev Show documentation
Show all versions of xapi-dev Show documentation
Everything needed to run a comprehensive dev environment.
Just type X_ and pick a service from autocomplete;
new dev modules will be added as they are built.
The only dev service not included in the uber jar is xapi-dev-maven,
as it includes all runtime dependencies of maven, adding ~4 seconds to build time,
and 6 megabytes to the final output jar size (without xapi-dev-maven, it's ~1MB).
The newest version!
package xapi.dev.ui.autoui;
import static java.lang.reflect.Modifier.FINAL;
import static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.STATIC;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.dev.javac.StandardGeneratorContext;
import com.google.gwt.dev.jjs.UnifyAstView;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.google.gwt.user.server.Base64Utils;
import com.google.gwt.util.tools.shared.Md5Utils;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import javax.annotation.Generated;
import javax.inject.Named;
import xapi.collect.impl.SimpleFifo;
import xapi.dev.source.ClassBuffer;
import xapi.dev.source.MethodBuffer;
import xapi.dev.source.SourceBuilder;
import xapi.source.write.MappedTemplate;
import xapi.ui.autoui.api.AlwaysTrue;
import xapi.ui.autoui.api.BeanValueProvider;
import xapi.ui.autoui.api.UiOptions;
import xapi.ui.autoui.api.UiRenderer;
import xapi.ui.autoui.api.UiRendererOptions;
import xapi.ui.autoui.api.UiRendererSelector;
import xapi.ui.autoui.api.UiRenderingContext;
public class AutoUiGenerator {
private static final JType[] EMPTY_PARAMS = new JType[0];
private static final Set PRIMITIVES = ImmutableSet.builder()
.add(Boolean.class.getName())
.add(Byte.class.getName())
.add(Character.class.getName())
.add(Short.class.getName())
.add(Integer.class.getName())
.add(Long.class.getName())
.add(Float.class.getName())
.add(Double.class.getName())
.add(String.class.getName())
.build();
final Map methods = new LinkedHashMap();
final String clsName;
final Map> factories = new HashMap>();
final Map templates = new HashMap();
int ctxCnt;
final SourceBuilder out;
public static final String generateUiProvider(final TreeLogger logger, final UnifyAstView ast, final JClassType uiModel, final JClassType uiType) throws UnableToCompleteException {
final AutoUiGenerator ctx = new AutoUiGenerator(ast, uiModel, uiType);
String src = ctx.out.toString();
final String digest =
Base64Utils.toBase64(Md5Utils.getMd5Digest(src.getBytes()));
String name = ctx.out.getQualifiedName();
JClassType existing = ast.getTypeOracle().findType(name), winner = null;
int pos = 0;
while (true) {
winner = existing;
final String next = name+pos++;
existing = ast.getTypeOracle().findType(next);
if (existing == null) {
break;
} else {
name = next;
}
}
if (winner != null) {
// Only use the existing class if the source exactly matches what we just generated.
final Generated gen = existing.getAnnotation(Generated.class);
if (gen.value()[1].equals(digest)) {
return name;
}
}
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
ctx.out.getClassBuffer().setSimpleName(name.replace(ctx.out.getPackage()+".", ""));
ctx.out.getClassBuffer().addAnnotation("@"+
ctx.out.getImports().addImport(Generated.class)+"("+
"date=\""+df.format(new Date())+"\",\n" +
"value={\"" + AutoUiGenerator.class.getName()+"\","+
"\""+digest+"\"})");
final StandardGeneratorContext gen = ast.getGeneratorContext();
final PrintWriter pw = gen.tryCreate(logger, ctx.out.getPackage(), ctx.out.getClassBuffer().getSimpleName());
src = ctx.out.toString();
pw.print(src);
gen.commit(logger, pw);
gen.finish(logger);
logger.log(Type.INFO, src);
try {
return name;
} finally {
ctx.methods.clear();
ctx.factories.clear();
ctx.templates.clear();
ctx.out.destroy();
}
}
private AutoUiGenerator(final UnifyAstView ast, final JClassType uiModel, final JClassType uiType) {
clsName = uiType.getSimpleSourceName()+"_"+uiModel.getSimpleSourceName()+"_Factory";
out = new SourceBuilder(
"public final class "+clsName);
out.setPackage(uiModel.getPackage().getName());
out.setPayload(ast);
final JPrimitiveType voidType = ast.getProgram().getTypeVoid();
final SimpleFifo simpleNames = new SimpleFifo();
extractAllMethods(uiModel, voidType);
simpleNames.giveAll(methods.keySet());
final String uiCls = out.getImports().addImport(uiType.getErasedType().getQualifiedSourceName());
final ClassBuffer beanCls =
out.getClassBuffer().createInnerClass("private static final class Bean")
.setSuperClass(out.getImports().addImport(BeanValueProvider.class));
final List inits = new ArrayList();
if (uiModel.isAnnotationPresent(UiOptions.class)) {
final UiOptions opts = uiModel.getAnnotation(UiOptions.class);
if (opts.fields().length > 0) {
simpleNames.clear();
simpleNames.giveAll(opts.fields());
}
for (final UiRendererOptions renderer : opts.renderers()) {
inits.addAll(addClassRenderer(renderer, out, ast));
}
}
if (uiModel.isAnnotationPresent(UiRendererOptions.class)) {
inits.addAll(addClassRenderer(uiModel.getAnnotation(UiRendererOptions.class), out, ast));
}
for (final JMethod method : methods.values()) {
if (method.isAnnotationPresent(UiRendererOptions.class)) {
inits.addAll(addMethodRenderer(method, out, ast));
}
}
final MethodBuffer builder = out.getClassBuffer().createMethod(
"public static final "+ uiCls + " newUi()");
builder
.println("final Bean bean = new Bean();")
.println("final "+uiCls+" ui = new "+uiCls+"();")
.println("ui.setRenderers(new " + builder.addImport(UiRenderingContext.class) +"[]{")
.indent()
;
for (int i = 0, m = inits.size()-1; i <= m; i ++) {
final String init = inits.get(i);
builder
.print(init)
.println(i == m ? "" : ",");
}
builder
.outdent()
.println("});")
.returnValue("ui");
final MethodBuffer beanCtor = beanCls
.createConstructor(PRIVATE)
.print("setChildKeys(new String[]{");
if (!simpleNames.isEmpty()) {
beanCtor
.println()
.indentln("\""+simpleNames.join("\", \"")+"\"");
}
beanCtor.println("});");
final MethodBuffer valueProvider = beanCls
.createMethod("protected Object valueOf(String name, Object o)")
.setUseJsni(true)
.println("switch(name) {")
.indent()
.println("case 'this': return o;")
// TODO prefer @Named value
.print("case 'this.name()':")
.indentln("return [email protected]::getClass()()[email protected]::getName()();");
for (final String name : methods.keySet()) {
final JMethod method = methods.get(name);
valueProvider.println("case '"+name+".name()': return '"+name+"';");
final JClassType rootMethodClass = getMethodRoot(method);
final String rootMethod = beanCls.addImport(rootMethodClass.getQualifiedSourceName());
if (method.isPublic()) {
// An interface might be a lambda with defender methods...
// and they are currently broken from a jsni context
final String accessor = "access"+method.getName();
final String returnType = beanCls.addImport(method.getReturnType().getQualifiedSourceName());
out.getClassBuffer().createMethod("private static "+returnType+" "+accessor+"()")
.addParameters(rootMethod+" o")
.returnValue("o."+method.getName()+"()");
valueProvider.println("case '"+name+"': return @"+out.getQualifiedName()+"::"+accessor+"(" +
rootMethodClass.getJNISignature() + ")(o);");
} else {
valueProvider.println("case '"+name+"': return o.@"+rootMethodClass.getQualifiedSourceName()+"::" +
method.getName()+"()();");
}
}
valueProvider
.println("default: return @"+BeanValueProvider.class.getName()+
"::illegalArg(Ljava/lang/String;)(name);")
.outdent()
.println("};");
}
private JClassType getMethodRoot(final JMethod method) {
JClassType winner = method.getEnclosingType();
if (winner.isInterface() != null) {
for (final JClassType type : winner.getImplementedInterfaces()) {
if (type.findMethod(method.getName(), EMPTY_PARAMS) != null) {
if (type.isAssignableFrom(winner)) {
winner = type;
}
}
}
}
return winner.getErasedType();
}
private void extractAllMethods(final JClassType uiModel, final JPrimitiveType voidType) {
for (final JClassType type : uiModel.getFlattenedSupertypeHierarchy()) {
for (final JMethod method : type.getMethods()) {
if (isGetterMethod(method, voidType)) {
final String name = toSimpleName(method);
if (!methods.containsKey(name)) {
methods.put(name, method);
}
}
}
}
}
protected String getStaticInstance(final Class selector, final SourceBuilder out) {
String name = selector.getSimpleName().toUpperCase();
int tries = 0;
while (factories.containsKey(name)) {
if (factories.get(name).getCanonicalName().equals(selector.getCanonicalName())) {
return name;
}
name = selector.getSimpleName().toUpperCase() + tries++;
}
out.getClassBuffer().createField(
selector, name, PRIVATE | FINAL | STATIC)
.setInitializer("new "+out.getImports().addImport(selector)+"()");
factories.put(name, selector);
return name;
}
protected String toSimpleName(final JMethod method) {
if (method.isAnnotationPresent(Named.class)) {
return method.getAnnotation(Named.class).value();
}
final String name = method.getName();
if (name.startsWith("get") || name.startsWith("has")) {
if (name.length() > 3 && Character.isUpperCase(name.charAt(3))) {
return Character.toLowerCase(name.charAt(3)) +
(name.length() > 4 ? name.substring(4) : "");
}
} else if (name.startsWith("is")) {
if (name.length() > 2 && Character.isUpperCase(name.charAt(2))) {
return Character.toLowerCase(name.charAt(2)) +
(name.length() > 3 ? name.substring(3) : "");
}
}
return name;
}
private List addClassRenderer(final UiRendererOptions renderer, final SourceBuilder out, final UnifyAstView ast) {
return addRenderer(renderer.isWrapper()?"bean.rebaseAll()":"bean", "", renderer, out, ast);
}
private List addMethodRenderer(final JMethod method, final SourceBuilder out, final UnifyAstView ast) {
final UiRendererOptions anno = method.getAnnotation(UiRendererOptions.class);
final String name = toSimpleName(method);
return addRenderer("bean.rebase(\"" +name+"\")", name, anno, out, ast);
}
@SuppressWarnings("rawtypes")
private List addRenderer(final String bean, final String path, final UiRendererOptions renderer,
final SourceBuilder out, final UnifyAstView ast) {
final Class selector = renderer.selector();
final String template = getTemplate(renderer, out);
final List inits = new ArrayList();
for (final Class renderCls : renderer.renderers()) {
final String name = "ctx"+ctxCnt++;
final MethodBuffer provider = out.getClassBuffer().createMethod(PRIVATE | FINAL | STATIC, UiRenderingContext.class, name,
out.getImports().addImport(BeanValueProvider.class)+" bean");
final String ctxCls = out.getImports().addImport(UiRenderingContext.class);
provider.println(ctxCls +" ctx = new "+ctxCls+"(" +
getStaticInstance(renderCls, out)+");");
provider.println("ctx.setBeanProvider(bean);");
if (renderer.isHead()) {
provider.println("ctx.setHead(true);");
} else if (renderer.isTail()) {
provider.println("ctx.setTail(true);");
}
if (renderer.isWrapper()) {
provider.println("ctx.setWrapper(true);");
}
if (selector != AlwaysTrue.class) {
provider.println("ctx.setSelector("+getStaticInstance(selector, out)+");");
}
if (!"".equals(path)) {
provider.println("ctx.setName(\""+path+"\");");
}
provider.println(template);
provider.println("return ctx;");
inits.add(name+"("+bean+")");
}
return inits;
}
private String getTemplate(final UiRendererOptions renderer, final SourceBuilder out) {
final String t = renderer.template();
if (t.length() > 0) {
// Assemble all the keys to be used in the template.
final SimpleFifo replaceables = new SimpleFifo();
for (final String key : renderer.templatekeys()) {
if (t.contains(key)) {
replaceables.give(key);
}
}
final int depth = 10;
for (final String key : methods.keySet()) {
if (t.contains("${"+key+"}")) {
replaceables.give("${"+key+"}");
}
if (t.contains("${"+key+".name()}")) {
replaceables.give("${"+key+".name()}");
}
final JMethod method = methods.get(key);
if (isNonPrimitive(method, out.getPayload())) {
// create nested template field
}
}
final String initializer =
"new "+
out.getImports().addImport(MappedTemplate.class)+"(\""+Generator.escape(t)+"\", new String[]{"+
(replaceables.isEmpty()?"":"\""+replaceables.join("\", \"")+"\"")
+"});";
final int key;
if (templates.containsKey(initializer)) {
key = templates.get(initializer);
} else {
key = templates.size();
templates.put(initializer, key);
out.getClassBuffer().createField(MappedTemplate.class, "TEMPLATE_"+key, PRIVATE | STATIC | FINAL)
.setInitializer(initializer);
}
return "ctx.setTemplate(TEMPLATE_"+key+");";
} else {
return "";
}
}
private boolean isNonPrimitive(final JMethod method, final UnifyAstView ast) {
final JType returnType = method.getReturnType();
return returnType.isPrimitive() == null
&& !PRIMITIVES.contains(returnType.getQualifiedSourceName());
}
private boolean isGetterMethod(final JMethod method, final JPrimitiveType voidType) {
return method.isPublic() && method.getParameters().length == 0 && method.getReturnType() != voidType;
}
}