package xapi.elemental.impl; import elemental.client.Browser; import elemental.html.StyleElement; import xapi.annotation.inject.SingletonDefault; import xapi.collect.X_Collect; import xapi.collect.api.ClassTo; import xapi.collect.api.StringDictionary; import xapi.elemental.api.StyleCacheService; import xapi.log.X_Log; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays; /** * @author James X. Nelson (james@wetheinter.net) * Created on 2/6/16. */ @SingletonDefault(implFor = StyleCacheService.class) public class StyleCacheServiceDefault implements StyleCacheService { private StringDictionary<StyleElement> styleElems = X_Collect.newDictionary(StyleElement.class); @Override public StyleElement injectStyle( Class<? extends ClientBundle> bundle, Class<? extends CssResource>... styles ) { String key = toKey(bundle, styles); StyleElement style = styleElems.getValue(key); if (style == null) { style = generateStyle(bundle, styles); styleElems.setValue(key, style); } return (StyleElement) style.cloneNode(true); // we return clones, so shadowDom can efficiently inject style tags } protected StyleElement generateStyle(Class<? extends ClientBundle> bundle, Class<? extends CssResource> ... styles) { // First, we must find an instance of the bundle. Since it is too late to GWT.create, we must assume there is a constant field. ClientBundle resource; findResource: try { for (Field field : bundle.getFields()) { if (field.getType() == bundle) { resource = (ClientBundle) field.get(null); break findResource; } } X_Log.warn(getClass(), "Unable to find resource instance; runtime style injection will fail."); return null; } catch (Exception e) { X_Log.error(getClass(), "Attempting to perform runtime injection of CSS resources, but the class ", bundle, " was not enhanced to support reflection. Try invoking GwtReflect.magicClass(", bundle.getName(), ".class)"); return null; } ClassTo<CssResource> resources = X_Collect.newClassMap(CssResource.class); searchStyle: for (Class<? extends CssResource> style : styles) { for (Method method : bundle.getMethods()) { if (method.getReturnType() == style) { try { resources.put(style, (CssResource) method.invoke(resource)); continue searchStyle; } catch (Exception e) { X_Log.error(getClass(), "Attempt to inject resource ", style, " from ", bundle, " using method ", method.getName(), "failed", e); } } } // exact type match failed, settle for an assignable match for (Method method : bundle.getMethods()) { if (style.isAssignableFrom(method.getReturnType())) { try { resources.put(style, (CssResource) method.invoke(resource)); continue searchStyle; } catch (Exception e) { X_Log.error(getClass(), "Attempt to inject resource ", style, " from ", bundle, " using method ", method.getName(), "failed", e); } } } X_Log.warn(getClass(), "Unable to find an instance of ", style, " in the methods of ", bundle,". It will be skipped"); } // Now that we have all the unique style instances, lets condense them into unique results (since we allowed assignable matches) ClassTo<CssResource> unique = X_Collect.newClassMap(CssResource.class); for (CssResource css : resources.values()) { unique.put(css.getClass(), css); } resources.clear(); String s = ""; for (CssResource cssResource : unique.values()) { s += cssResource.getText(); } unique.clear(); final StyleElement styleElem = createStyle(s); return styleElem; } @Override public void registerStyle( Class<? extends ClientBundle> bundle, String css, Class<? extends CssResource>... styles ) { String key = toKey(bundle, styles); final StyleElement style = createStyle(css); styleElems.setValue(key, style); } private StyleElement createStyle(String css) { final StyleElement style = Browser.getDocument().createStyleElement(); style.setTextContent(css); return style; } private String toKey(Class<? extends ClientBundle> bundle, Class<? extends CssResource>[] styles) { String s = bundle.getName(); String[] names = new String[styles.length]; for (int i = styles.length; i-->0;) { names[i] = styles[i].getName(); } Arrays.sort(names); for (String name : names) { s += ":" + name; } return s; } }