All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xapi.dev.ui.autoui.AutoUiGenerator Maven / Gradle / Ivy

Go to download

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;
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy