package xapi.dev.components; import elemental.dom.Element; import xapi.collect.X_Collect; import xapi.collect.api.ClassTo; import xapi.collect.api.Fifo; import xapi.collect.api.IntTo; import xapi.collect.impl.SimpleFifo; import xapi.components.api.ShadowDomStyle; import xapi.dev.source.ClassBuffer; import xapi.dev.source.MethodBuffer; import xapi.dev.source.PrintBuffer; import xapi.dev.source.SourceBuilder; import xapi.dev.source.SourceTransform; import xapi.fu.In1; import xapi.fu.In2.In2Unsafe; import xapi.inject.X_Inject; import xapi.source.X_Modifier; import xapi.util.X_String; import static xapi.fu.In2Out1.with2; import com.google.gwt.core.ext.GeneratorContext; 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.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import java.io.PrintWriter; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * @author James X. Nelson (james@wetheinter.net) * Created on 2/7/16. */ public class ShadowDomStyleInjectorGenerator { protected class InjectionResult { private Class<? extends ClientBundle> bundle; private LinkedHashSet<Class<? extends CssResource>> resources; private String providerClass; public InjectionResult(ShadowDomStyle style, String genClass) { providerClass = genClass; bundle = style.bundle(); resources = new LinkedHashSet<>(); for (Class<? extends CssResource> cls : style.styles()) { resources.add(cls); } } public Class<? extends ClientBundle> getBundle() { return bundle; } public String getProviderClass() { return providerClass; } public Set<Class<? extends CssResource>> getResources() { return resources; } protected boolean matches(ShadowDomStyle style) { if (style.bundle() != bundle) { return false; } if (style.styles().length != resources.size()) { return false; } final Set<Class<? extends CssResource>> test = new LinkedHashSet<>(resources); final Set<Class<? extends CssResource>> seen = new HashSet<>(); // in case someone sends the same class twice... for (Class<? extends CssResource> cls : style.styles()) { // make sure we ignore duplicate classes if (seen.add(cls)) { // make sure we have the class you are requesting if (!test.remove(cls)) { return false; } } } // make sure we don't have more classes than you (else we would add extra css, and our signatures would not line up). return test.isEmpty(); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof InjectionResult)) return false; final InjectionResult that = (InjectionResult) o; if (bundle != null ? !bundle.equals(that.bundle) : that.bundle != null) return false; return resources != null ? resources.equals(that.resources) : that.resources == null; } @Override public int hashCode() { int result = bundle != null ? bundle.hashCode() : 0; result = 31 * result + (resources != null ? resources.hashCode() : 0); return result; } public String inject(String into) { return providerClass + ".inject(" + into + ");"; } } private ClassTo<String> resourceProviders = X_Collect.newClassMap(String.class); private ClassTo.Many<InjectionResult> cssInjectors = X_Collect.newClassMultiMap(InjectionResult.class); public SourceTransform generateShadowStyles( TreeLogger logger, Fifo<ShadowDomStyle> sharedStyles, Fifo<ShadowDomStyle> localStyles, GeneratorContext context ) { Fifo<SourceTransform> injectors = new SimpleFifo<>(); extractInjectors(logger, sharedStyles, context, injectors::give); extractInjectors(logger, localStyles, context, injectors::give); return into-> { StringBuilder b = new StringBuilder(); injectors.transform(with2(SourceTransform::transform, into), b::append); return b.toString(); }; // "styleProvider.addTo("+into+")" } private void extractInjectors( TreeLogger logger, Fifo<ShadowDomStyle> allStyle, GeneratorContext context, In1<SourceTransform> callback ) { Map<ShadowDomStyle, String> missingProviders = new LinkedHashMap<>(); allStyle.out(style->{ final IntTo<InjectionResult> existing = cssInjectors.get(style.bundle()); if (existing.forMatches( // test if we match. Shorthand for injector->injector.matches(style) with2(InjectionResult::matches, style), item->callback.in(into->into + into + ");")) ) { return; // returns from this lambda, which is performing iteration. } // No luck finding an existing match. Lets start generating... final String location = style.resourceInstanceExpression(); if (location.isEmpty()) { // No user-specified injection location; lets find one missingProviders.put(style, findResourceInstance(logger, style, context)); } else { // We trust the user to give us a fully qualified expression missingProviders.put(style, location); } }); // Anything in the missingProviders map must be generated. final In2Unsafe<ShadowDomStyle, String> buildProvider = (style, location)->{ final InjectionResult result = generateInjector(logger, style, context, location); cssInjectors.add(style.bundle(), result); callback.in(result::inject); }; missingProviders.entrySet().forEach(buildProvider.mapAdapter()); } private InjectionResult generateInjector( TreeLogger logger, ShadowDomStyle style, GeneratorContext context, String location ) throws UnableToCompleteException { Set<String> methodsToUse = new LinkedHashSet<>(); final Method[] methods = style.bundle().getMethods(); for (Class<? extends CssResource> cls : style.styles()) { boolean success = false; for (Method method : methods) { if (method.getParameters().length == 0 && cls.isAssignableFrom(method.getReturnType())) { methodsToUse.add(method.getName()); success = true; } } if (!success) { logger.log(Type.ERROR, "Unable to find CssResource type " + cls.getCanonicalName()+" in resource class " + style.bundle().getCanonicalName()); throw new UnableToCompleteException(); } } // Alright, we have our list of method names to invoke on the resource to create our css. // Lets find/create a provider class. String genPkg = style.bundle().getPackage().getName(); StringBuilder nameBuilder = new StringBuilder(style.bundle().getCanonicalName().replace(genPkg+".", "").replaceAll("[.]", "__")); nameBuilder.append("__With"); methodsToUse.forEach(name->nameBuilder.append("_").append(name)); String genName = nameBuilder.toString(); String genClass = (genPkg.isEmpty() ? "" : genPkg + ".") + genName; // Search for an existing type... final InjectionResult result = new InjectionResult(style, genClass); if (context.getTypeOracle().findType(genPkg, genName) != null) { return result; } // Check if it was already generated, but is not yet committed to the TypeOracle. final PrintWriter pw = context.tryCreate(logger, genPkg, genName); if (pw == null) { // The type was already generated, but not yet committed. return result; } // No luck, lets generate! SourceBuilder src = new SourceBuilder("public class " + genName) .setPackage(genPkg); String ele = src.addImport(Element.class); String resourceType = src.addImport(result.bundle); // TODO: right now, you must have this on your classpath and in your gwt module.... // Lets move this out into a leaner module (like this one) // Or have our method supply a StyleService, and just use that. String X_Elemental = src.addImport("xapi.elemental.X_Elemental"); List<String> styleClasses = new ArrayList<>(); for (Class<? extends CssResource> cls : style.styles()) { styleClasses.add(src.addImport(cls)); } In1<PrintBuffer> printResourceClasses = o-> styleClasses.forEach(type-> o .print(", ") .print(type) .print(".class") ); // create a static block of code to register our css with the elemental service // we may later move the service to a method parameter, and use RunOnce semantics to enable just-in-time injection. final ClassBuffer out = src.getClassBuffer(); out .println("static {") .indent() .println(resourceType + " res = " + location + ";") .println("String css = \"\";") ; methodsToUse.forEach(name->{ out .println("css += res." + name + "().getText();") // .indentln(".replaceAll(\"[\\\\\\\\]00\", \"\\\\\\\\\");") ; }); out .print(X_Elemental) .print(".getElementalService().registerStyle(") .print(resourceType) .print(".class, css") ; printResourceClasses.in(out); out .println(");") .outdent() .println("}") ; final MethodBuffer inject = out.createMethod("public static void inject(" + ele + " into)"); inject .println("into.appendChild(") .indent() .print(X_Elemental) .print(".getElementalService().injectStyle(") .print(resourceType) .print(".class") ; printResourceClasses.in(inject); inject .println(")") .outdent() .println(");"); // All done! let's write out and commit our result String code = src.toString(); pw.append(code); context.commit(logger, pw); return result; } private String findResourceInstance(TreeLogger logger, ShadowDomStyle style, GeneratorContext context) { final Class<? extends ClientBundle> cls = style.bundle(); String location = resourceProviders.get(cls); if (X_String.isNotEmpty(location)) { return location; } for (Field field : cls.getFields()) { if (field.getType() == cls) { // Lets use this field. return cls.getCanonicalName() + "." + field.getName(); } } for (Method method : cls.getMethods()) { if (X_Modifier.isStatic(method.getModifiers()) && method.getReturnType() == cls) { // Lets use this static method. return cls.getCanonicalName() + "." + method.getName(); } } // Now that we have tried exact matches, lets try for assignability matches for (Field field : cls.getFields()) { if (cls.isAssignableFrom(field.getType())) { // Lets use this field. return cls.getCanonicalName() + "." + field.getName(); } } for (Method method : cls.getMethods()) { if (X_Modifier.isStatic(method.getModifiers()) && cls.isAssignableFrom(method.getReturnType())) { // Lets use this static method. return cls.getCanonicalName() + "." + method.getName(); } } // Ok, no providers available on fields. We are going to have to generate a provider instance... // Luckily, X_Inject.singleton will do this nicely for us, creating a single instance backed by GWT.create location = X_Inject.class.getCanonicalName() + ".singleton(" + cls.getCanonicalName()+".class)"; resourceProviders.put(cls, location); return location; } }