package xapi.dev.ui.html;
import xapi.annotation.compile.Import;
import xapi.collect.X_Collect;
import xapi.collect.api.IntTo;
import xapi.collect.api.IntTo.Many;
import xapi.dev.source.ClassBuffer;
import xapi.dev.source.MethodBuffer;
import xapi.dev.source.PrintBuffer;
import xapi.dev.source.SourceBuilder;
import xapi.source.X_Source;
import xapi.time.impl.RunOnce;
import xapi.ui.api.StyleService;
import xapi.ui.html.X_Html;
import xapi.ui.html.api.Css;
import xapi.ui.html.api.Css.CssFile;
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.api.ReceivesValue;
import static com.google.gwt.reflect.rebind.ReflectionUtilType.findType;
import static xapi.dev.ui.html.AbstractHtmlGenerator.existingTypesUnchanged;
import static xapi.dev.ui.html.AbstractHtmlGenerator.findExisting;
import static xapi.dev.ui.html.AbstractHtmlGenerator.saveGeneratedType;
import static xapi.dev.ui.html.AbstractHtmlGenerator.toHash;
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.shared.GWT;
import com.google.gwt.dev.jjs.UnifyAstView;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import java.net.URL;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
public class CssInjectorGenerator implements CreatesContextObject<HtmlGeneratorResult>{
static final String GENERATED_SUFFIX = "_InjectCss";
int ctxCnt;
private String clsName;
private final HtmlGeneratorContext htmlGen;
private final SourceBuilder<UnifyAstView> out;
public static final HtmlGeneratorResult generateSnippetProvider(final TreeLogger logger, final UnifyAstView ast, final JClassType templateType) throws UnableToCompleteException {
final String simpleName = templateType.getSimpleSourceName()+GENERATED_SUFFIX;
// Check if there is an existing type, and that it's generated hashes match our input type.
final String inputHash = toHash(ast, templateType.getQualifiedSourceName());
// Generate a new result
final CssInjectorGenerator ctx = new CssInjectorGenerator(simpleName, ast, templateType);
// Save the result, possibly to a new file if there are existing implementations using our default name
return ctx.generate(logger, ast, templateType, inputHash, simpleName);
}
@Override
public HtmlGeneratorResult newContext(final JClassType winner, final String pkgName, final String name) {
return new HtmlGeneratorResult(winner, pkgName, name);
}
private CssInjectorGenerator(final String className, final UnifyAstView ast, final JClassType templateType) throws UnableToCompleteException {
this.clsName = className;
this.htmlGen = new HtmlGeneratorContext(templateType);
this.out = new SourceBuilder<UnifyAstView>("public class "+clsName)
.setPackage(templateType.getPackage().getName())
.setPayload(ast);
}
private HtmlGeneratorResult generate(final TreeLogger logger, final UnifyAstView ast, final JClassType injectionType, final String inputHash, final String simpleName) throws UnableToCompleteException {
final HtmlGeneratorResult existingType = findExisting(ast, this,
injectionType.getPackage().getName(),
X_Source.qualifiedName(injectionType.getPackage().getName(), simpleName));
final HtmlGeneratorResult existingResult = existingTypesUnchanged(logger, ast, existingType, inputHash);
if (existingResult != null) {
// If our inputs are unchanged, and the target type exists, just reuse it w/out regenerating
return existingResult;
}
clsName = existingType.getFinalName();
final ClassBuffer cls = out.getClassBuffer();
final String cssService = cls.addImport(StyleService.class);
final String setOnce = cls.addImportStatic(RunOnce.class, "setOnce");
final String receiver = cls.addImport(ReceivesValue.class);
// Create a public method for callers to access
final MethodBuffer inject = cls.createMethod("public static void inject("+cssService+" serv)");
// Now fill out the body with our css injection that will run only once.
final IntTo.Many<Style> styles = X_Collect.newIntMultiMap(Style.class);
final Set<Class<? extends ClientBundle>> resourceTypes = new HashSet<Class<? extends ClientBundle>>();
final Set<String> importTypes = new LinkedHashSet<>();
final Set<CssFile> files = new LinkedHashSet<>();
fillStyles(logger, styles, files, resourceTypes, importTypes, injectionType);
// Now compute any supertypes that might want injection too
for (final JClassType superType : injectionType.getFlattenedSupertypeHierarchy()) {
if (!superType.getQualifiedSourceName().equals(injectionType.getQualifiedSourceName())) {
if (hasStyle(superType)) {
importTypes.add(superType.getQualifiedSourceName());
}
}
}
// Always print calls to inject style for any explicit imports, and for all supertypes
// If these types lack style, their inject methods will no-op and get erased by the compiler
for (String importable : importTypes) {
importable = inject.addImport(importable);
final String injectCss = inject.addImportStatic(X_Html.class, "injectCss");
inject.println(injectCss+"("+importable+".class, serv);");
}
if (styles.isEmpty() && resourceTypes.isEmpty()) {
// Nothing to import. Lets encourage inlining by printing an empty method :D
logger.log(getLogLevel(), "Skipped style injection for style-less class "+injectionType.getQualifiedSourceName());
} else {
// This type actually has some @Style rules or ClientBundle instances to inject.
inject.println("ONCE.set(serv);");
// Now create a private field that will allow us to implement set-once semantics
final PrintBuffer init = cls.createField(receiver+"<"+cssService+">", "ONCE")
.makeFinal()
.makeStatic()
.makePrivate()
.getInitializer()
.println(setOnce+"(new "+receiver+"<"+cssService+">(){")
.indent()
.println("@Override")
.println("public void set("+cssService+" serv) {");
// Tear off a print buffer for the body of the method
final PrintBuffer body = init.makeChild();
// Close the initializer now, so we don't wind up with close-brace-soup later on.
init.println("}")
.outdent()
.println("});");
// Print all our manually defined @Style attribute
for (final IntTo<Style> styleSet : styles.forEach()) {
int priority = Integer.MAX_VALUE;
final StringBuilder sheetStyle = new StringBuilder();
final StringBuilder extraStyle = new StringBuilder();
for (final Style style : styleSet.forEach()) {
try {
int pos = AbstractHtmlGenerator.fillStyles(null, sheetStyle, extraStyle, style);
priority = Math.min(pos, priority);
} catch (final Exception e) {
logger.log(Type.ERROR, "Error calculating styles while generating css for "
+ injectionType.getQualifiedSourceName(), e);
throw new UnableToCompleteException();
}
}
if (sheetStyle.length() > 0) {
body.println("serv.addCss(\""+Generator.escape(sheetStyle.toString())+"\", "+priority+");");
}
if (extraStyle.length() > 0) {
body.println("serv.addCss(\""+Generator.escape(extraStyle.toString())+"\", Integer.MIN_VALUE);");
}
}
for (CssFile file : files) {
final Class<? extends CssResource>[] interfaces = file.interfaces();
final String[] fileNames = file.value();
final ClassLoader cl = Thread.currentThread().getContextClassLoader();
for (int i = 0, m = fileNames.length; i < m; i++ ) {
String fileName = fileNames[i];
if (fileName.startsWith("/")) {
// This is to be treated as an absolute location
fileName = fileName.substring(1);
} else if (fileName.startsWith("./")) {
// Treat this file as relative to the first CssResource interface provided
if (interfaces.length == 0) {
logger.log(Type.ERROR, "Cannot use a relative @CssFile location using ./ without also supplying a CssResource in the interfaces annotation member. " +
"You supplied: "+file+".\n" +
"./uris are relative to the css interface\n" +
"/uris are absolute to the classpath\n" +
"and all unprefixed uris are relative to the location where the @CssFile annotation is found ("+injectionType.getQualifiedSourceName()+")");
throw new UnableToCompleteException();
}
fileName = interfaces[0].getPackage().getName().replace('.', '/') + "/" + fileName;
} else {
// Treat this file as relative to the type we are injecting
fileName = injectionType.getPackage().getName().replace('.', '/') + "/" + fileName;
}
final URL url = cl.getResource(fileName);
if (url == null) {
logger.log(Type.ERROR, "Unable to find resource "+fileName+" on the classpath. Path derived from "+file+" on "+injectionType.getQualifiedSourceName());
throw new UnableToCompleteException();
}
fileNames[i] = fileName;
}
if (interfaces.length == 0) {
// No interfaces. We just want to dump the whole stylesheet
} else if (interfaces.length == 1) {
// exactly one interface, just use it directly
} else {
// more than one interface, we need to generate a container type
}
}
// Print injections and initializations for all ClientBundle classes
int numResource = 0;
final JClassType cssResource = findType(ast.getTypeOracle(), CssResource.class);
for (final Class<? extends ClientBundle> resourceType : resourceTypes) {
final JClassType type = findType(ast.getTypeOracle(), resourceType);
assert type != null : "Unable to inject ClientBundle class: "+resourceType;
final String resource = inject.addImport(resourceType);
final String gwtCreate = inject.addImportStatic(GWT.class, "create");
final String name = "res"+numResource++;
body.println(resource +" "+name+" = "+gwtCreate+"("+resource+".class);");
// Now, search the declared type for methods that are instances of CssResource, to .ensureInjected()
for (final JMethod method : type.getMethods()) {
if (!method.isStatic()) {
final JClassType asType = method.getReturnType().isClassOrInterface();
if (asType != null && asType.isAssignableTo(cssResource)) {
body.println(name+"."+method.getName()+"().ensureInjected();");
}
}
}
}
}
try {
return saveGeneratedType(logger, getLogLevel(), getClass(), ast, out, existingType, inputHash);
} finally {
ast.getGeneratorContext().finish(logger);
}
}
private boolean hasStyle(final JClassType superType) {
final Css css = superType.getAnnotation(Css.class);
if (css != null) {
return true;
}
final Style style = superType.getAnnotation(Style.class);
if (style != null && style.names().length > 0) {
return true;
}
final Html html = superType.getAnnotation(Html.class);
if (html != null) {
if (html.css().length > 0) {
return true;
}
if (hasStyle(html.body())) {
return true;
}
if (hasStyle(html.templates())) {
return true;
}
}
final HtmlTemplate template = superType.getAnnotation(HtmlTemplate.class);
if (template != null && hasStyle(template)) {
return true;
}
final El el = superType.getAnnotation(El.class);
return el != null && hasStyle(el);
}
private boolean hasStyle(final HtmlTemplate ... templates) {
for (final HtmlTemplate template : templates) {
if (template.imports().length > 0 || template.references().length > 0) {
return true;
}
}
return false;
}
private boolean hasStyle(final El ... body) {
for (final El el : body) {
for (final Style style : el.style()) {
if (style.names().length > 0) {
return true;
}
}
if (el.imports().length > 0) {
return true;
}
}
return false;
}
private void fillStyles(
final TreeLogger logger,
final Many<Style> styles,
final Set<CssFile> files,
final Set<Class<? extends ClientBundle>> resourceTypes,
final Set<String> importTypes,
final JClassType templateType) {
final Html html = templateType.getAnnotation(Html.class);
if (html != null) {
fillStyles(logger, styles, files, resourceTypes, html.css());
fillStyles(logger, styles, importTypes, html.body());
fillStyles(logger, styles, importTypes, html.templates());
}
final HtmlTemplate template = templateType.getAnnotation(HtmlTemplate.class);
if (template != null) {
fillStyles(logger, styles, importTypes, template);
}
final El el = templateType.getAnnotation(El.class);
if (el != null) {
fillStyles(logger, styles, importTypes, el);
}
final Css css = templateType.getAnnotation(Css.class);
if (css != null) {
fillStyles(logger, styles, files, resourceTypes, css);
}
final Style style = templateType.getAnnotation(Style.class);
if (style != null && style.names().length > 0) {
styles.add(style.priority(), style);
}
}
private void fillStyles(final TreeLogger logger, final Many<Style> styles, final Set<String> importTypes, final HtmlTemplate ... templates) {
for (final HtmlTemplate template : templates) {
for (final Import importable : template.imports()) {
importTypes.add(importable.value().getCanonicalName());
}
for (final Class<?> importable : template.references()) {
importTypes.add(importable.getCanonicalName());
}
}
}
private void fillStyles(final TreeLogger logger, final Many<Style> styles, final Set<String> importTypes, final El ... body) {
for (final El item : body) {
for (final Style style : item.style()) {
if (style.names().length > 0) {
styles.add(style.priority(), style);
}
}
for (final Import importable : item.imports()) {
importTypes.add(importable.value().getCanonicalName());
}
}
}
private void fillStyles(final TreeLogger logger,
final Many<Style> styles,
final Set<CssFile> files,
final Set<Class<? extends ClientBundle>> resourceTypes,
final Css ... csses) {
for (final Css css : csses) {
for (final Style style : css.style()) {
if (style.names().length > 0) {
styles.add(style.priority(), style);
}
}
for (final Class<? extends ClientBundle> cls : css.resources()) {
resourceTypes.add(cls);
}
for (final CssFile file : css.files()) {
files.add(file);
}
}
}
protected Type getLogLevel() {
return Type.DEBUG;
}
}