package xapi.dev.ui.html;
import xapi.annotation.compile.Generated;
import xapi.annotation.compile.Import;
import xapi.collect.X_Collect;
import xapi.collect.api.IntTo;
import xapi.collect.api.StringTo;
import xapi.dev.source.SourceBuilder;
import xapi.except.NotYetImplemented;
import xapi.source.X_Source;
import xapi.source.write.MappedTemplate;
import xapi.source.write.StringerMatcher;
import xapi.ui.html.api.Css;
import xapi.ui.html.api.El;
import xapi.ui.html.api.Html;
import xapi.ui.html.api.HtmlTemplate;
import xapi.ui.html.api.Style;
import xapi.util.X_String;
import xapi.util.api.ConvertsValue;
import xapi.util.impl.LazyProvider;
import static xapi.collect.X_Collect.newStringMap;
import static xapi.ui.html.api.HtmlSnippet.appendTo;
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.dev.javac.StandardGeneratorContext;
import com.google.gwt.dev.jjs.UnifyAstView;
import com.google.gwt.user.server.Base64Utils;
import com.google.gwt.util.tools.shared.Md5Utils;
import javax.inject.Provider;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.function.Predicate;
public abstract class AbstractHtmlGenerator <Ctx extends HtmlGeneratorResult> implements CreatesContextObject<Ctx> {
protected static final String KEY_FROM = "from";
private static final Provider<Boolean> isDev = new LazyProvider<>(new Provider<Boolean>() {
@Override
public Boolean get() {
return true;
}
});
protected static <Ctx extends HtmlGeneratorResult> Ctx existingTypesUnchanged(final TreeLogger logger,
final UnifyAstView ast, final Ctx result, final String verify) {
if (isDev.get()) {
// During development, never reuse existing types, as we're likely changing generators
return null;
}
try {
if (result.getSourceType() == null) {
return null;
}
final Generated gen = result.getSourceType().getAnnotation(Generated.class);
if (gen == null) {
return null;
}
final String hash = gen.value()[gen.value().length-1];
if (verify.equals(hash)) {
return result;
}
} catch (final Exception e) {
logger.log(Type.WARN, "Unknown error calculating change hashes", e);
}
return null;
}
protected static String toHash(final UnifyAstView ast, final String ... types) throws UnableToCompleteException {
final StringBuilder b = new StringBuilder();
for (final String type : types) {
b.append(ast.searchForTypeBySource(type).toSource());
}
return Base64Utils.toBase64(Md5Utils.getMd5Digest(b.toString().getBytes()));
}
protected String clsName;
protected String documentType;
protected final HtmlGeneratorContext htmlGen;
protected final SourceBuilder<UnifyAstView> out;
protected boolean renderAllChildren;
protected String[] renderOrder;
public AbstractHtmlGenerator(final String clsName, final JClassType templateType, final UnifyAstView ast) {
this.clsName = clsName;
htmlGen = new HtmlGeneratorContext(templateType);
this.out = new SourceBuilder<UnifyAstView>("public class "+clsName)
.setPackage(templateType.getPackage().getName())
.setPayload(ast);
}
protected void addEl(final String name, final El el) {
htmlGen.addEl(name, el);
}
protected void addHtml(final String name, final Html html, final IntTo<String> elOrder) {
if (html == null) {
return;
}
elOrder.add(name);
}
public void addImport(final Import importType) {
if (importType.staticImport().length()==0) {
out.getImports().addImport(importType.value());
} else {
out.getImports().addStaticImport(importType.value(), importType.staticImport());
}
}
protected void clear() {
htmlGen.clear();
}
/**
* @param key
* @param accessor
* @param template
* @return
*/
protected ConvertsValue<String, String> createProvider(final String key,
final String accessor, final HtmlTemplate template) {
switch (key) {
case HtmlTemplate.KEY_VALUE:
return new ConvertsValue<String, String>() {
@Override
public String convert(final String from) {
if (accessor.equals(El.DEFAULT_ACCESSOR)) {
return "".equals(key) ? KEY_FROM :
KEY_FROM+"."+key+(key.endsWith("()") ? "" : "()");
} else {
return accessor;
}
}
};
default:
return new ConvertsValue<String, String>() {
@Override
public String convert(final String from) {
return from.startsWith(key) ?
from.substring(key.length()) :
from;
}
};
case HtmlTemplate.KEY_CHILDREN:
case HtmlTemplate.KEY_PARENT:
case HtmlTemplate.KEY_CONTEXT:
throw new NotYetImplemented("Key "+key+" not yet implemented in "+getClass()+".createProvider()");
}
}
/**
* @param text
* @return
*/
protected String escape(final String text) {
return Generator.escape(text);
}
@SuppressWarnings("unchecked")
protected String escape(String text, final String key, final String accessor) {
if (text.length() == 0) {
return "";
}
final HtmlGeneratorNode node = htmlGen.allNodes.get(key);
if (node.hasTemplates()) {
final StringTo<Object> references = newStringMap(Object.class);
for(final HtmlTemplate template : node.getTemplates()) {
final String name = template.name();
if (template.inherit() && template.name().length() == 0) {
continue;
}
if (!references.containsKey(name)) {
references.put(name.equals("") ? "$this" : name,
createProvider(key, accessor, template));
}
}
if (!references.isEmpty()) {
final StringerMatcher matcher = new StringerMatcher() {
@Override
public String toString(final Object o) {
return escape(String.valueOf(o));
}
@Override
public Predicate<String> matcherFor(String value) {
return null;
}
};
final MappedTemplate apply = new MappedTemplate(text, matcher, references.keyArray()) {
@Override
protected Object retrieve(final String key, final Object object) {
final ConvertsValue<String, String> converter = (ConvertsValue<String, String>) object;
return converter.convert(key);
}
};
text = apply.applyMap(references.entries());
}
}
return replace$value(text, key, accessor);
}
protected String replace$value(String text, final String key, final String accessor) {
String ref = null;
int ind = text.indexOf("$value");
if (ind == -1) {
return "\""+escape(text)+"\"";
}
final StringBuilder b = new StringBuilder("\"");
int was;
for (;;){
was = ind;
ind = text.indexOf("$value");
if (ind == -1) {
break;
}
if (ref == null) {
if (accessor.equals(El.DEFAULT_ACCESSOR)) {
ref = getDefaultKey() + ("".equals(key) ? "" : "."+key+"()");
} else {
ref = accessor;
}
}
if (ind > 0) {
b.append(escape(text.substring(0, ind)));
}
if ("".equals(ref)) {
b.append("\"");
} else {
b.append("\" + "+ ref + " + \"");
}
text = text.substring(6);
}
if (was > -1) {
b.append(escape(text));
}
b.append("\"");
return b.toString();
}
private String getDefaultKey() {
return KEY_FROM;
}
protected void fillMembers(JClassType templateType, final IntTo<String> elOrder) {
for (final JClassType type : templateType.getFlattenedSupertypeHierarchy()) {
for (final JMethod method : type.getMethods()) {
if (method.isAnnotationPresent(Html.class) || method.isAnnotationPresent(El.class)) {
final String name = toSimpleName(method);
if (!elOrder.contains(name)) {
elOrder.add(name);
// Html html = method.getAnnotation(Html.class);
// addHtml(name, html, elOrder);
// addEl(name, method.getAnnotation(El.class));
htmlGen.addCss(name, method.getAnnotation(Css.class));
htmlGen.addImport(name, method.getAnnotation(Import.class));
htmlGen.addMethod(name, method);
}
}
}
}
templateType = templateType.getSuperclass();
if (templateType != null && !templateType.getQualifiedSourceName().equals("java.lang.Object")) {
fillMembers(templateType, elOrder);
}
}
protected static int fillStyles(final Appendable immediateStyle,
final Appendable sheetStyle, final Appendable extraStyle, final Style... styles) throws IOException {
int priority = Integer.MAX_VALUE;
for (final Style style : styles) {
priority = Math.min(priority, style.priority());
final String[] names = style.names();
if (names.length == 0) {
if (immediateStyle != null) {
String extra = appendTo(immediateStyle, style);
if (X_String.isNotEmpty(extra)) {
extraStyle.append(extra).append("\n");
}
}
} else if (sheetStyle != null){
for (int i = 0, m = names.length; i < m; i++) {
if (i > 0) {
sheetStyle.append(", ");
}
sheetStyle.append(names[i]);
}
sheetStyle.append("{\n");
String extra = appendTo(sheetStyle, style);
sheetStyle.append("\n}\n");
if (X_String.isNotEmpty(extra)) {
extraStyle.append(extra).append("\n");
}
}
}
return priority;
}
protected static <Ctx> Ctx findExisting(final UnifyAstView ast, final CreatesContextObject<Ctx> creator, final String pkgName, String name) {
if (name.indexOf('.') == -1) {
name = X_Source.qualifiedName(pkgName, name);
}
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) {
return creator.newContext(winner, pkgName, name);
}
}
}
public Iterable<El> getElements(final String key) {
return htmlGen.allNodes.get(key).getElements();
}
protected Type getLogLevel() {
return Type.DEBUG;
}
public Iterable<HtmlTemplate> getTemplates(final String key) {
return htmlGen.allNodes.get(key).getTemplates();
}
protected void initialize() {
// clear();
renderAllChildren = true;
renderOrder = new String[0];
documentType = Html.ROOT_ELEMENT;
final IntTo<String> elOrder = X_Collect.newList(String.class);
Html html = null;
for (final JClassType type : htmlGen.cls.getFlattenedSupertypeHierarchy()) {
if (html == null && type.isAnnotationPresent(Html.class)) {
html = type.getAnnotation(Html.class);
}
if (type.isAnnotationPresent(Import.class)) {
addImport(type.getAnnotation(Import.class));
}
}
if (html != null) {
documentType = html.document();
renderOrder = html.renderOrder();
addHtml("", html, elOrder);
renderAllChildren = html.renderOrder().length == 0;
}
fillMembers(htmlGen.cls, elOrder);
if (renderAllChildren) {
renderOrder = elOrder.toArray();
}
}
protected static <Ctx extends HtmlGeneratorResult> Ctx saveGeneratedType(
final TreeLogger logger, final Type logLevel, final Class<?> generatorClass, final UnifyAstView ast,
final SourceBuilder<?> out, final Ctx result, final String inputHash) throws UnableToCompleteException {
final String name = result.getFinalName();
String src = out.toString();
final String digest =
Base64Utils.toBase64(Md5Utils.getMd5Digest(src.getBytes()));
if (result.getSourceType() != null) {
// Only use the existing class if the generated source exactly matches what we just generated.
final Generated gen = result.getSourceType().getAnnotation(Generated.class);
if (gen != null && gen.value()[1].equals(digest)) {
return result;
}
}
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
out.getClassBuffer().setSimpleName(name.replace(out.getPackage()+".", ""));
out.getClassBuffer().addAnnotation("@"+
out.getImports().addImport(Generated.class)+"("+
"date=\""+df.format(new Date())+"\",\n" +
"value={\"" + generatorClass.getName()+"\","+
"\""+digest+"\", \""+inputHash+"\"})");
final StandardGeneratorContext gen = ast.getGeneratorContext();
final PrintWriter pw = gen.tryCreate(logger, out.getPackage(), out.getClassBuffer().getSimpleName());
src = out.toString();
pw.print(src);
gen.commit(logger, pw);
if (logger.isLoggable(logLevel)) {
logger.log(logLevel, src);
}
try {
return result;
} finally {
out.destroy();
}
}
protected String toSimpleName(final JMethod method) {
// if (method.isAnnotationPresent(Named.class))
// return method.getAnnotation(Named.class).value();
return method.getName();
}
protected String toSimpleName(final String name) {
// 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;
}
}