package xapi.dev.components;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseException;
import com.github.javaparser.ast.expr.UiContainerExpr;
import xapi.annotation.inject.SingletonDefault;
import xapi.annotation.inject.SingletonOverride;
import xapi.collect.api.Fifo;
import xapi.collect.impl.SimpleFifo;
import xapi.components.api.IsControlledComponent;
import xapi.components.api.IsWebComponent;
import xapi.components.api.JsoConsumer;
import xapi.components.api.JsoSupplier;
import xapi.components.api.NativelySupported;
import xapi.components.api.ShadowDom;
import xapi.components.api.ShadowDomPlugin;
import xapi.components.api.ShadowDomStyle;
import xapi.components.api.ShadowDomStyles;
import xapi.components.api.WebComponent;
import xapi.components.api.WebComponentCallback;
import xapi.components.api.WebComponentFactory;
import xapi.components.api.WebComponentMethod;
import xapi.components.impl.JsFunctionSupport;
import xapi.components.impl.JsSupport;
import xapi.components.impl.WebComponentBuilder;
import xapi.components.impl.WebComponentSupport;
import xapi.dev.source.ClassBuffer;
import xapi.dev.source.MethodBuffer;
import xapi.dev.source.SourceBuilder;
import xapi.dev.source.SourceTransform;
import xapi.dev.ui.ContainerMetadata;
import xapi.inject.X_Inject;
import xapi.io.X_IO;
import xapi.log.X_Log;
import xapi.log.api.LogLevel;
import xapi.source.X_Source;
import xapi.ui.html.api.Css;
import xapi.ui.html.api.El;
import xapi.ui.html.api.Html;
import xapi.ui.html.api.Style;
import static java.lang.reflect.Modifier.PRIVATE;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.UnsafeNativeLong;
import com.google.gwt.core.client.js.JsProperty;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.IncrementalGenerator;
import com.google.gwt.core.ext.RebindMode;
import com.google.gwt.core.ext.RebindResult;
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.*;
import com.google.gwt.dev.util.collect.Sets;
import com.google.gwt.thirdparty.guava.common.collect.LinkedHashMultimap;
import com.google.gwt.thirdparty.guava.common.collect.Multimap;
import javax.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class WebComponentFactoryGenerator extends IncrementalGenerator {
private static ThreadLocal<ShadowDomStyleInjectorGenerator> shadowDomGenerator = new ThreadLocal<ShadowDomStyleInjectorGenerator>() {
@Override
protected ShadowDomStyleInjectorGenerator initialValue() {
return new ShadowDomStyleInjectorGenerator();
}
};
static ShadowDomStyleInjectorGenerator getStyleInjectorGenerator() {
return shadowDomGenerator.get();
}
private enum BuiltInType {
// Uses non-standard bean-casing so we can use .name() to generate code to
// match methods
// in the WebComponentSupport class
created, attached, detached, attributeChanged;
public static BuiltInType find(final JMethod method) throws UnableToCompleteException {
switch (method.getName()) {
// TODO use complete signature here
case "onAttached":
return attached;
case "onDetached":
return detached;
case "onCreated":
return created;
case "onAttributeChanged":
return attributeChanged;
default:
return null;
}
}
}
private static class MethodData {
private final String accessorName;
private String name;
private String getterName;
private String setterName;
private String getterClass;
private String setterClass;
private BuiltInType type;
private boolean enumerable = false;
private boolean configurable = true;
private boolean writeable = false;
public String valueClass;
public boolean mapToAttribute;
public boolean useJsniWildcard;
public MethodData(final String accessorName, final String name) {
this.accessorName = accessorName;
this.name = name;
}
private boolean isProperty() {
return getterName != null || setterName != null;
}
}
private static final Pattern BEAN_NAME = Pattern.compile("(is|get|set)(.+)");
private JClassType stringType;
private JClassType webComponentCallback;
private static final String BOX_HELPER = "@" + JsSupport.class.getName()
+ "::";
private static final String JSO_PARAM =
"Lcom/google/gwt/core/client/JavaScriptObject;";
@Override
public RebindResult generateIncrementally(final TreeLogger logger,
final GeneratorContext context, final String typeName)
throws UnableToCompleteException {
final JClassType type = context.getTypeOracle().findType(typeName);
final WebComponent component = type.getAnnotation(WebComponent.class);
final Fifo<ShadowDomStyle> styles = extractSharedStyles(type);
if (component == null) {
logger.log(Type.ERROR, "Type " + type.getQualifiedSourceName()
+ " missing required annotation, " + WebComponent.class.getName());
throw new UnableToCompleteException();
}
if (component.tagName().indexOf('-') == -1) {
logger.log(Type.ERROR,
"WebComponent for " + type.getQualifiedSourceName()
+ " has invalid tag name " + component.tagName() + "; "
+ "Custom elements must contain the - character");
throw new UnableToCompleteException();
}
final String pkg = type.getPackage().getName();
final String simple = type.getQualifiedSourceName().replace(pkg + ".", "");
final String factoryName = toFactoryName(simple);
final String qualifiedName = pkg + "." + factoryName;
// TODO reenable this once we add strong hashing support to ensure types
// have not changed.
// if (context.tryReuseTypeFromCache(qualifiedName)) {
// return new RebindResult(RebindMode.USE_ALL_CACHED, qualifiedName);
// }
final PrintWriter pw = context.tryCreate(logger, pkg, factoryName);
if (pw == null) {
logger.log(logLevel(), "Reusing existing class " + qualifiedName);
return new RebindResult(RebindMode.USE_EXISTING, qualifiedName);
}
final SourceBuilder<ContainerMetadata> sourceBuilder = new SourceBuilder<ContainerMetadata>
("public final class " + factoryName)
.setPackage(pkg);
final ClassBuffer out =
sourceBuilder.getClassBuffer()
.addInterface(
WebComponentFactory.class.getCanonicalName() + "<" + simple + ">");
final String builder = out.addImport(WebComponentBuilder.class);
final String support = out.addImport(WebComponentSupport.class);
final String jso = out.addImport(JavaScriptObject.class);
final String selfType = out.addImport(typeName);
final String proto = generatePrototypeAccessor(out, component.extendProto(), jso);
stringType = context.getTypeOracle().findType("java.lang.String");
webComponentCallback = context.getTypeOracle().findType(WebComponentCallback.class.getName());
final Multimap<String, MethodData> methods = LinkedHashMultimap.create();
final boolean hasCallbacks = type.isAssignableTo(webComponentCallback);
final List<JClassType> flattened = new ArrayList<JClassType>(type.getFlattenedSupertypeHierarchy());
for (int i = flattened.size(); i --> 0; ) {
final JClassType iface = flattened.get(i);
generateFunctionAccessors(logger, context, iface, methods, hasCallbacks);
}
final String supplier = out.addImport(Supplier.class);
out
.createField(supplier + "<" + simple + ">", "ctor")
.makeStatic()
.makePrivate();
// Initialize the web component in a static block
out
.println("static {")
.indent();
if (hasCssToInject(type)) {
final String injectCss = out.addImportStatic("xapi.elemental.X_Elemental.injectCss");
out.println(injectCss+"("+selfType+".class);");
}
out
.println(builder + " builder = " + builder + ".create(" + proto + ");");
if (component.extendProto().length > 1) {
out
.println("builder.setExtends(\"" + component.extendProto()[1] + "\");");
}
final Set<String> seen = new HashSet<>();
for (int i = flattened.size(); i --> 0; ) {
final JClassType key = flattened.get(i);
for (final MethodData method : methods.get(key.getQualifiedSourceName())) {
if (method.isProperty()) {
printPropertyAccess(logger, out, method, builder, seen);
} else {
printValueAccess(logger, out, method, builder, seen);
}
}
}
final String inject = out.addImport(X_Inject.class);
for (Class<? extends ShadowDomPlugin> pluginClass : component.plugins()) {
String plugin = out.addImport(pluginClass);
out
.println("builder.addShadowDomPlugin(")
.indent()
.print(inject)
.print(isSingleton(pluginClass) ? ".instance" : ".singleton")
.println("(" + plugin +".class)")
.outdent()
.println(");");
}
ShadowDom[] shadowDoms = component.shadowDom();
if (shadowDoms.length > 0) {
for (ShadowDom shadowDom : shadowDoms) {
// Calculate if we need to do any css injection.
SourceTransform shadowStyle = null;
if (shadowDom.styles().length > 0 || styles.isNotEmpty()) {
final Fifo<ShadowDomStyle> localStyles = new SimpleFifo<>();
localStyles.giveAll(shadowDom.styles());
shadowStyle = getStyleInjectorGenerator().generateShadowStyles(logger, styles, localStyles, context);
}
for (String template : shadowDom.value()) {
ContainerMetadata metadata = createMetadata();
if (shadowStyle != null) {
metadata.addModifier(shadowStyle);
}
metadata.setSourceBuilder(sourceBuilder);
template = resolveTemplate(logger, template, context, type, metadata);
out
.print("builder.addShadowRoot(\"")
.print(Generator.escape(template))
.print("\"")
.println()
.println(", shadow -> {")
.indent();
metadata.applyModifiers(out, "shadow");
out
.println("return shadow;")
.outdent()
.println("});"); // just close out the shadowroot call
}
}
}
out
.print("ctor = " + support + ".register(")
.print("\"" + component.tagName() + "\"")
.println(", builder.build());")
.outdent()
.println("}")
.createMethod(
"public " + simpleName(type) + " newComponent()")
.returnValue("ctor.get()");
// Print the querySelector method.
final MethodBuffer querySelector = out.createMethod("public String querySelector()");
if (component.extendProto().length > 1) {
// We are extending an existing method.
querySelector.returnValue("\""+component.extendProto()[1]+"[is="+component.tagName()+"]\"");
} else {
querySelector.returnValue("\""+component.tagName()+"\"");
}
final String src = sourceBuilder.toString();
logger.log(logLevel(typeName), "\nWeb Component Factory: \n" + src);
pw.println(src);
context.commit(logger, pw);
return new RebindResult(RebindMode.USE_ALL_NEW, qualifiedName);
}
protected ContainerMetadata createMetadata() {
return new ContainerMetadata();
}
public static String toFactoryName(String simple) {
return simple.replace('.', '_') + "_WebComponentFactory";
}
private Type logLevel() {
return Type.WARN;
}
protected boolean isSingleton(Class<? extends ShadowDomPlugin> pluginClass) {
return
pluginClass.getAnnotation(Singleton.class) != null ||
pluginClass.getAnnotation(SingletonOverride.class) != null ||
pluginClass.getAnnotation(SingletonDefault.class) != null
;
}
private Fifo<ShadowDomStyle> extractSharedStyles(JClassType type) {
final Fifo<ShadowDomStyle> list = new SimpleFifo<>();
final ShadowDomStyles styles = type.getAnnotation(ShadowDomStyles.class);
if (styles != null) {
list.giveAll(styles.value());
}
final ShadowDomStyle style = type.getAnnotation(ShadowDomStyle.class);
if (style != null) {
list.giveAll(style);
}
return list;
}
private String resolveTemplate(TreeLogger logger, String template, GeneratorContext context, JClassType type, ContainerMetadata metadata) throws UnableToCompleteException {
String asString;
boolean wasHtml = false;
if (template.trim().startsWith("<")) {
// raw html / xapi template.
asString = template;
} else {
// resource to load
if (!template.startsWith("/")) {
// Relative resource
template = "/"+type.getPackage().getName().replace('.', '/')+"/"+template;
wasHtml = template.endsWith(".html");
}
try (
InputStream resource = context.getResourcesOracle().getResourceAsStream(template.substring(1))
) {
if (resource == null) {
logger.log(Type.ERROR, "Unable to find shadow root bundle "+template);
throw new UnableToCompleteException();
}
asString = X_IO.toStringUtf8(resource);
} catch (IOException e) {
logger.log(Type.ERROR, "Error generating shadow root bundle "+template, e);
throw new UnableToCompleteException();
}
}
try {
final UiContainerExpr container = JavaParser.parseUiContainer(asString);
// The template parsed.
// We are going to need to transform it into a few different forms:
// 1) The actual html to inject, which we return
// 2) Any stylesheets or message bundles generated by the template
// 3) A transform function that can operate on the shadow dom element at runtime.
metadata.setContainer(container);
metadata.setType(type.getErasedType().getQualifiedSourceName());
for (JClassType iface : type.getImplementedInterfaces()) {
if (iface.getQualifiedSourceName().equals(IsWebComponent.class.getCanonicalName())) {
final JClassType elementType = iface.isParameterized().getTypeArgs()[0];
metadata.setElementType(elementType.getErasedType().getQualifiedSourceName());
} else if (iface.getQualifiedSourceName().equals(IsControlledComponent.class.getCanonicalName())) {
final JClassType elementType = iface.isParameterized().getTypeArgs()[0];
final JClassType componentType = iface.isParameterized().getTypeArgs()[1];
final JClassType controllerType = iface.isParameterized().getTypeArgs()[2];
metadata.setElementType(elementType.getErasedType().getQualifiedSourceName());
metadata.setComponentType(componentType.getErasedType().getQualifiedSourceName());
final JClassType erased = controllerType.getErasedType();
final JPackage pkg = erased.getPackage();
String pkgName = pkg == null ? "" : pkg.getName();
String clsName = X_Source.removePackage(pkgName, erased.getQualifiedSourceName());
metadata.setControllerType(pkgName, clsName);
}
}
// replace the string version with a modified one that is safe to inject.
MethodReferenceReplacementVisitor.mutateExpression(container, metadata);
return generateComponentBinding(logger, context, type, metadata, container);
} catch (ParseException e) {
if (!wasHtml && metadata.isAllowedToFail()) {
X_Log.warn(getClass(), "Unable to parse html as xapi UI. Treating template as raw html. Set -Dxapi.log.level=TRACE to see the template");
X_Log.trace(asString);
if (X_Log.loggable(LogLevel.TRACE)) {
if (!asString.equals(template)) {
X_Log.trace("From template", template);
}
}
X_Log.warn(e);
return asString;
} else {
logger.log(Type.ERROR, "Unparseable xapi template:");
if (!asString.equals(template)) {
logger.log(Type.ERROR, "loaded from " + template);
}
logger.log(Type.ERROR, asString, e);
throw new UnableToCompleteException();
}
}
}
protected String generateComponentBinding(
TreeLogger logger,
GeneratorContext context,
JClassType type,
ContainerMetadata metadata,
UiContainerExpr container
) {
return container.toSource();
}
@Override
public long getVersionId() {
return 0;
}
protected Type logLevel(final String typeName) {
return
// BooleanPickerElement.class.getName().equals(typeName) ? Type.INFO : Type.TRACE;
Type.DEBUG;
}
private String accessorName(final JMethod method) {
final StringBuilder b = new StringBuilder(method.getName());
for (final JParameter param : method.getParameters()) {
b.append('_').append(param.getName());
}
return b.toString();
}
private String debean(final String name) {
final Matcher matcher = BEAN_NAME.matcher(name);
if (matcher.matches()) {
final String match = matcher.group(2);
return Character.toLowerCase(match.charAt(0)) + (match.length() > 0 ? match.substring(1) : "");
}
return name;
}
private void generateDefaultFunctionAccessor(final TreeLogger logger,
final JMethod method, final MethodData data, final ClassBuffer cls, final Set<String> helpers) {
final String qualified = method.getEnclosingType().getQualifiedSourceName();
final String typeName = cls.addImport(qualified);
cls.addImport(JavaScriptObject.class);
final MethodBuffer out =
cls
.createMethod(
"public static JavaScriptObject " + data.accessorName + "()")
.addParameters(typeName + " o")
.makeJsni()
.addImports(JavaScriptObject.class)
.print("var func = o.@" + qualified + "::" + method.getName() + "(");
List<JType> rawParams = new ArrayList<>();
for (JType param : method.getParameterTypes()) {
if (param instanceof JParameterizedType) {
param = ((JParameterizedType)param).getRawType();
}
if (param instanceof JTypeParameter) {
param = ((JTypeParameter)param).getErasedType();
}
rawParams.add(param);
}
if (data.useJsniWildcard) {
out.print("*");
} else {
for (JType param : rawParams) {
out.print(param.getJNISignature());
}
}
final StringBuilder params = new StringBuilder();
final Map<Character, String> boxers = new LinkedHashMap<>();
char paramName = 'a';
for (final JType param : rawParams) {
// out.print(param.getErasedType().getJNISignature());
if (params.length() > 0) {
params.append(',');
}
params.append(paramName);
final String boxingPrefix = maybeBoxPrefix(logger, param, false, out, cls, helpers);
final String boxingSuffix = maybeBoxSuffix(logger, param, false, out);
boxers.put(paramName, boxingPrefix + paramName + boxingSuffix);
paramName++;
}
out
.println(");")
.print("return @")
.print(JsFunctionSupport.class.getName())
.print("::maybeEnter(*)")
.println("(function(" + params + "){")
.indent();
final String boxReturnPrefix = maybeBoxPrefix(logger, method.getReturnType(), true, out, cls, helpers);
final String boxReturnSuffix = maybeBoxSuffix(logger, method.getReturnType(), true, out);
final boolean hasReturn = method.getReturnType() != JPrimitiveType.VOID;
if (hasReturn) {
// Non void return type; we may need to box/unbox the result
out.print("var ret = " + boxReturnPrefix);
}
if (hasReturn) {
out.indent();
}
out
.println("func(this");
for (final Character c : boxers.keySet()) {
BuiltInType type = data.type;
if (type == null) {
type = BuiltInType.attributeChanged;
}
out.print(", ");
switch (type) {
case attributeChanged:
out.println(boxers.get(c));
break;
default:
// for onCreated, onAttached and onDetached, we supply this reference
// as the element argument to the method, as a convenience
out.println("this");
break;
}
}
out.print(")");
if (hasReturn) {
out.println().outdent().print(boxReturnSuffix);
}
out.println(";");
if (hasReturn) {
out.println("return ret;");
}
out
.outdent()
.println("});");
}
private void generateFunctionAccessors(final TreeLogger logger,
final GeneratorContext context, final JClassType iface,
final Multimap<String, MethodData> results, final boolean hasCallbacks) throws UnableToCompleteException {
if (iface.getMethods().length == 0) {
return;
}
final JParameterizedType asParam = iface.isParameterized();
final JClassType[] paramTypes = asParam == null ? new JClassType[0] : asParam.getTypeArgs();
final String pkg = iface.getPackage().getName();
final String simple = iface.getQualifiedSourceName().replace(pkg + ".", "");
final String result = simple.replace('.', '_') + simplify(paramTypes) + "_JsFunctionAccess";
final String name = iface.getQualifiedSourceName();
final String qualified = pkg + "." + result;
SourceBuilder<PrintWriter> source = null;
// TODO reenable once we can check input source files for freshness
// if (!context.tryReuseTypeFromCache(qualified)) {
final PrintWriter pw = context.tryCreate(logger, pkg, result);
if (pw != null) {
source = new SourceBuilder<PrintWriter>
("public final class " + result)
.setPackage(pkg)
.setPayload(pw);
source.getClassBuffer().createConstructor(PRIVATE);
}
final Set<String> helpers = new LinkedHashSet<>();
for (final JMethod method : iface.getMethods()) {
if (method.isStatic()) {
continue;
}
if (method.getAnnotation(NativelySupported.class) != null) {
// Do not blow away natively supported methods!
continue;
}
if (method.getEnclosingType() == iface) {
Collection<MethodData> existing = results.get(name);
MethodData data = new MethodData(accessorName(method), method.getName());
if (hasCallbacks) {
data.type = BuiltInType.find(method);
data.useJsniWildcard = true;
}
final WebComponentMethod metaData =
method.getAnnotation(WebComponentMethod.class);
if (metaData != null) {
if (!metaData.name().isEmpty()) {
data.name = metaData.name();
}
data.useJsniWildcard = metaData.useJsniWildcard();
data.mapToAttribute = metaData.mapToAttribute();
data.configurable = metaData.configurable();
data.enumerable = metaData.enumerable();
data.writeable = metaData.writeable();
}
if (method.isDefaultMethod()) {
data.valueClass = qualified;
existing = Sets.add(new LinkedHashSet<>(existing), data);
results.putAll(name, existing);
// A default method! Let's generate a method to extract a javascript
// function that will correctly handle un/boxing when passing values.
if (source != null) {
generateDefaultFunctionAccessor(logger, method, data, source.getClassBuffer(), helpers);
}
} else {
// An abstract method should be treated like a JsType method;
// if it's a getter or a setter, try to use element attributes
final String debeaned = debean(method.getName());
if (metaData == null || metaData.name().isEmpty()) {
data.name = debeaned;
}
for (final MethodData previous : results.values()) {
if (previous.name.equals(data.name)) {
if (previous.isProperty()) {
data = previous;
} else {
logger.log(Type.ERROR, "Duplicate property definitions found for web component member with "
+ "name [" + previous.name + "]. Conflict between " + previous.accessorName + " and "
+ data.accessorName);
throw new UnableToCompleteException();
}
}
}
final JsProperty prop = method.getAnnotation(JsProperty.class);
if (prop != null || isBeanFormat(method)) {
// Explicitly a js property, or it looks like a bean. Lets wire it
// up!
if (method.getParameterTypes().length == 0) {
// Getter
data.getterName = "get_" + method.getName();
data.getterClass = qualified;
if (source != null) {
generateGetter(logger, debeaned, data, method, source, helpers);
}
} else {
// Setter
data.setterName = "set_" + method.getName();
data.setterClass = qualified;
if (source != null) {
generateSetter(logger, debeaned, data, method, source, helpers);
}
}
existing = Sets.add(new LinkedHashSet<MethodData>(existing), data);
results.putAll(name, existing);
} else {
logger.log(Type.WARN, "Unable to generate web component implementation for "
+ method.getReadableDeclaration() +
" of " + method.getEnclosingType().getQualifiedSourceName()
+ ". The underlying method will only work correctly "
+ "if supplied by the underlying native element");
}
}
}
}
if (source != null) {
final String src = source.toString();
source.getPayload().println(src);
logger.log(logLevel(iface.getQualifiedSourceName()), src);
context.commit(logger, source.getPayload());
}
}
private void generateGetter(final TreeLogger logger, final String debeaned, final MethodData data, final JMethod method,
final SourceBuilder<PrintWriter> source, final Set<String> helpers) {
final MethodBuffer out = source.getClassBuffer()
.createMethod("public static JavaScriptObject get_" + method.getName())
.addImports(JavaScriptObject.class)
.makeJsni();
final String boxingPrefix = maybeBoxPrefix(logger, method.getReturnType(), true, out, source.getClassBuffer(), helpers);
final String boxingSuffix = maybeBoxSuffix(logger, method.getReturnType(), true, out);
out
.println("return function() {")
.indent()
.print("return ");
if (data.mapToAttribute) {
out.print(boxingPrefix)
// our boxing code will automatically handle primitive conversion
.print("this.getAttribute('" + debeaned + "')")
.print(boxingSuffix);
} else {
out.print(boxingPrefix)
.print("this.__" + debeaned)
.print(boxingSuffix);
}
out
.println(";")
.outdent()
.println("}");
}
private String generatePrototypeAccessor(final ClassBuffer out,
final String[] extendProto,
final String jso) {
out
.createMethod("private static native " + jso + " proto()")
.setUseJsni(true)
.println("return Object.create(" + extendProto[0] + ".prototype);");
return "proto()";
}
private void generateSetter(final TreeLogger logger, final String debeaned, final MethodData data, final JMethod method,
final SourceBuilder<PrintWriter> source, final Set<String> helpers) {
final MethodBuffer out = source.getClassBuffer()
.createMethod("public static JavaScriptObject set_" + method.getName())
.addImports(JavaScriptObject.class)
.makeJsni();
final String boxingPrefix = maybeBoxPrefix(logger, method.getParameterTypes()[0], false, out, source.getClassBuffer(), helpers);
final String boxingSuffix = maybeBoxSuffix(logger, method.getParameterTypes()[0], false, out);
final boolean fluent = method.getReturnType() != JPrimitiveType.VOID;
assert !fluent || isAssignableFrom(method.getReturnType(), method.getEnclosingType()) : "Cannot implement fluent method "
+ method.getJsniSignature();
out
.println("return function(i) {")
.indent();
if (data.mapToAttribute) {
out
.println("var val = i == null ? null : "+boxingPrefix + "i" + boxingSuffix+";")
.println("if (val == null) {")
.indentln("this.removeAttribute('"+debeaned+"');")
.println("} else {")
.indentln("this.setAttribute('" + debeaned + "', val);")
.println("}");
} else {
out
.print("this.__").print(debeaned)
.print(" = i == null ? null : ")
.print(boxingPrefix).print("i").print(boxingSuffix)
.println(";");
}
if (fluent) {
out.println("return this;");
}
out
.outdent()
.println("}");
}
private boolean hasCssAnnotations(final JClassType type) {
if (hasStyleAnnotations(type)) {
return true;
}
final Html html = type.getAnnotation(Html.class);
if (html != null) {
if (html.css().length > 0) {
return true;
}
for (final El el : html.body()) {
if (hasStyle(el)) {
return true;
}
}
}
final El el = type.getAnnotation(El.class);
if (hasStyle(el)) {
return true;
}
for (final JMethod method : type.getMethods()) {
if (hasStyle(method.getAnnotation(El.class))) {
return true;
}
if (hasStyleAnnotations(method)) {
return true;
}
}
return false;
}
private boolean hasCssToInject(final JClassType type) {
for (final JClassType subtype : type.getFlattenedSupertypeHierarchy()) {
if (hasCssAnnotations(subtype)) {
return true;
}
}
return false;
}
private boolean hasStyle(final El el) {
return el != null && el.style().length > 0;
}
private boolean hasStyleAnnotations(final HasAnnotations member) {
return member.isAnnotationPresent(Css.class) || member.isAnnotationPresent(Style.class);
}
private boolean isAssignableFrom(final JType returnType, final JClassType enclosingType) {
return returnType instanceof JClassType ? enclosingType.isAssignableFrom((JClassType) returnType) : false;
}
private boolean isBeanFormat(final JMethod method) {
if (method.getParameterTypes().length == 0) {
// This, if anything, is a getter.
return method.getReturnType() != JPrimitiveType.VOID;
}
return method.getParameterTypes().length == 1;
}
private String maybeBoxPrefix(final TreeLogger logger, final JType type, final boolean jsToJava, final MethodBuffer out, final ClassBuffer enclosing, final Set<String> helpers) {
if (type.isPrimitive() == null) {
// The type is not primitive. If it maps to the object form of a
// primitive, we must box it if primitive
switch (type.getQualifiedSourceName()) {
case "elemental.js.util.JsArrayOfString":
if (jsToJava) {
return BOX_HELPER + "unboxArrayOfString("
+ JSO_PARAM + "Ljava/lang/String;)(";
} else {
return BOX_HELPER + "boxArray("
+ JSO_PARAM + "Ljava/lang/String;)(";
}
case "elemental.js.util.JsArrayOfInt":
if (jsToJava) {
return BOX_HELPER + "unboxArrayOfInt("
+ JSO_PARAM + "Ljava/lang/String;)(";
} else {
return BOX_HELPER + "boxArray("
+ JSO_PARAM + "Ljava/lang/String;)(";
}
case "elemental.js.util.JsArrayOfNumber":
if (jsToJava) {
return BOX_HELPER + "unboxArrayOfNumber("
+ JSO_PARAM + "Ljava/lang/String;)(";
} else {
return BOX_HELPER + "boxArray("
+ JSO_PARAM + "Ljava/lang/String;)(";
}
case "elemental.js.util.JsArrayOfBoolean":
if (jsToJava) {
return BOX_HELPER + "unboxArrayOfNumber("
+ JSO_PARAM + "Ljava/lang/String;)(";
} else {
return BOX_HELPER + "boxArray("
+ JSO_PARAM + "Ljava/lang/String;)(";
}
case "java.lang.Long":
case "java.lang.Boolean":
case "java.lang.Byte":
case "java.lang.Short":
case "java.lang.Character":
case "java.lang.Integer":
case "java.lang.Float":
case "java.lang.Double":
return BOX_HELPER + "box" + simpleName(type) + "("
+ JSO_PARAM + ")(";
default:
if (type instanceof JClassType) {
final JClassType asClass = ((JClassType) type).getErasedType();
try {
if (jsToJava) {
JMethod valueOf;
try {
// Prefer fromString(String) as this allows enums to override .toString()
// fromString is also preferable as it better matches .toString()
valueOf = asClass.getMethod("fromString", new JType[] { stringType });
} catch (final NotFoundException e) {
// Also accept enum default method, valueOf
valueOf = asClass.getMethod("valueOf", new JType[] { stringType });
}
// In order to guard against null/empty values being sent to these methods,
// we will actually generate a helper method to perform the empty-value check,
// and simply return null if the appropriate value is not sent.
// If you want to handle empty values, use an empty string, which will still be
// passed into the associated methods.
final String typeName = enclosing.addImport(asClass.getQualifiedSourceName());
final String methodName = asClass.getSimpleSourceName() +"_"+valueOf.getName()+"_Helper";
if (helpers.add(methodName)) {
final MethodBuffer helper = enclosing.createMethod
("private static "+typeName+" "+methodName+"(String value)")
.makeJsni()
.print("return value == null ? null : ");
if (valueOf.isStatic()) {
// if valueOf is static, we can just invoke it directly
helper.println("@" + asClass.getQualifiedSourceName() + "::"+valueOf.getName()+"(Ljava/lang/String;)(value);");
} else {
// however, if fromString is instance level, we must construct a
// new instance
if (asClass.getConstructor(new JType[0]) == null) {
logger.log(
Type.WARN,
"Found method "+valueOf.getName()+"(String) in type "
+ asClass.getQualifiedSourceName()
+ ", but could not use it for autoboxing because the method is "
+ "instance level and there is no zero-arg constructor available "
+ "to instantiate the given type");
helper.println("null;");
return "";
} else {
// we only support 0-arg constructors here
helper.println("@" + asClass.getQualifiedSourceName() + "::new()().@"
+ asClass.getQualifiedSourceName() + "::"+valueOf.getName()+"(Ljava/lang/String;)(value)");
}
}
}
return "@" + enclosing.getQualifiedName()+"::"+methodName+"(Ljava/lang/String;)(";
}
} catch (final NotFoundException ignored) {
// If there is no valueOf(String) method,
// we just don't perform any boxing.
}
}
}
} else {
// The type is primitive, we must unbox whatever is given to us
switch (type.isPrimitive()) {
case BOOLEAN:
return BOX_HELPER + "unboxBoolean(" + JSO_PARAM + ")(";
case BYTE:
return BOX_HELPER + "unboxByte(" + JSO_PARAM + ")(";
case SHORT:
return BOX_HELPER + "unboxShort(" + JSO_PARAM + ")(";
case CHAR:
return BOX_HELPER + "unboxCharacter(" + JSO_PARAM + ")(";
case INT:
return BOX_HELPER + "unboxInteger(" + JSO_PARAM + ")(";
case LONG:
out.addAnnotation(UnsafeNativeLong.class);
if (jsToJava) {
return BOX_HELPER + "unboxLongNative(" + JSO_PARAM + ")(";
}
return BOX_HELPER + "unboxLong(" + JSO_PARAM + ")(";
case FLOAT:
return BOX_HELPER + "unboxFloat(" + JSO_PARAM + ")(";
case DOUBLE:
return BOX_HELPER + "unboxDouble(" + JSO_PARAM + ")(";
default:
}
}
return "";
}
private String maybeBoxSuffix(final TreeLogger logger, final JType type, final boolean jsToJava, final MethodBuffer out) {
if (type.isPrimitive() == null) {
// The type is not primitive. If it maps to the object form of a
// primitive, we must box it if primitive
switch (type.getQualifiedSourceName()) {
case "elemental.js.util.JsArrayOfString":
case "elemental.js.util.JsArrayOfInt":
case "elemental.js.util.JsArrayOfNumber":
case "elemental.js.util.JsArrayOfBoolean":
if (jsToJava) {
return ", this.joiner)";
} else {
return ", i && i.joiner)";
}
case "java.lang.Long":
case "java.lang.Boolean":
case "java.lang.Byte":
case "java.lang.Short":
case "java.lang.Character":
case "java.lang.Integer":
case "java.lang.Float":
case "java.lang.Double":
return ")";
default:
if (type instanceof JClassType) {
final JClassType asClass = ((JClassType) type).getErasedType();
try {
boolean hasFromString = false;
final boolean hasValueOf = false;
try {
asClass.getMethod("fromString", new JType[] { stringType });
hasFromString = true;
} catch (final NotFoundException ignored) {}
if (jsToJava) {
if (hasFromString) {
return ")";
}
asClass.getMethod("valueOf", new JType[] { stringType });
return ")";
} else {
JMethod name;
if (hasFromString) {
try {
name = asClass.getMethod("toString", new JType[0]);
} catch (final NotFoundException e) {
name = asClass.getMethod("name", new JType[0]);
}
} else {
name = asClass.getMethod("name", new JType[0]);
}
if (name.isStatic()) {
// if name is static, we can't invoke it as a suffix, and, it
// really should never be static
logger.log(Type.WARN, "Unable to use method "+name.getName()+"() from " + asClass.getQualifiedSourceName() +
" in web component factory");
return "";
} else {
return ".@" + asClass.getQualifiedSourceName() + "::"+name.getName()+"()()";
}
}
} catch (final NotFoundException ignored) {
// If there is no valueOf(String) method,
// we just don't perform any boxing.
}
}
}
} else {
// The type is primitive, we must unbox whatever is given to us
switch (type.isPrimitive()) {
case BOOLEAN:
case BYTE:
case SHORT:
case CHAR:
case INT:
case LONG:
case FLOAT:
case DOUBLE:
return ")";
default:
}
}
return "";
}
private void printPropertyAccess(final TreeLogger logger, final ClassBuffer staticOut, final MethodData method, final String builder, final Set<String> seen) {
String name = method.name;
final String constName = "CONST_"+name.toUpperCase();
if (seen.add(constName)) {
staticOut.createField(String.class, constName)
.makeStatic()
.makeFinal()
.makePrivate()
.setInitializer("\"" + method.name + "\"");
}
name = "applyProperty_"+method.name;
final MethodBuffer out = staticOut.createMethod("private static " + builder + " " + name)
.addParameters(builder+" builder");
staticOut.println(name+"(builder);");
out
.print("builder.addProperty(")
.println(constName+", ");
if (method.getterName != null) {
final String jsoSupplier = staticOut.addImport(JsoSupplier.class);
final String cls = staticOut.addImport(method.getterClass);
out.println("new " + jsoSupplier + "(" + cls + "." + method.getterName + "()), ");
} else {
out.println("null, ");
}
if (method.setterName != null) {
final String jsoConsumer = staticOut.addImport(JsoConsumer.class);
final String cls = staticOut.addImport(method.setterClass);
out.println("new " + jsoConsumer + "(" + cls + "." + method.setterName + "()), ");
} else {
out.println("null, ");
}
out
.print(method.enumerable + ", ")
.println(method.configurable + ");");
out.returnValue("builder");
}
private void printValueAccess(final TreeLogger logger, final ClassBuffer staticOut, final MethodData method, final String builder, final Set<String> seen) {
String name = method.name;
final String constName = "CONST_"+name.toUpperCase();
if (seen.add(constName)) {
staticOut.createField(String.class, constName)
.makeStatic()
.makeFinal()
.makePrivate()
.setInitializer("\"" + method.name + "\"");
}
if (!seen.add(name)) {
if (method.type == null) {
// built-ins it's ok to have multiples
logger.log(Type.WARN, "Found duplicate key for "+method.name+" in "+staticOut.getQualifiedName()+"; "
+ "one of these methods will be overridden and discarded.");
// TODO: fix all warnings and escalate this to a compile-breaking error
}
int suffix = 0;
while (seen.contains(name+suffix)) {
suffix++;
}
name = name+suffix;
seen.add(name);
}
name = "applyValue_"+name;
final MethodBuffer out = staticOut.createMethod("private static " + builder + " " + name)
.addParameters(builder+" builder");
staticOut.println(name+"(builder);");
final String shortName = out.addImport(method.valueClass);
if (method.type == null) {
out
.print("builder.addValue(")
.print(constName+", ")
.print(shortName + "." + method.accessorName + "(null),")
.print(method.enumerable + ", ")
.print(method.configurable + ", ")
.println(method.writeable + ");");
} else {
// This is a built-in type. We should attach the correct callback.
out
.print("builder."+method.type.name() + "Callback(")
.print(shortName + "." + method.accessorName + "(null)")
.println(");");
}
out.returnValue("builder");
}
private String simpleName(final JType type) {
String binary = type.getQualifiedBinaryName();
final int last = binary.lastIndexOf('.');
if (last != -1) {
binary = binary.substring(last + 1);
}
return binary.replace('$', '.');
}
private String simplify(final JClassType[] typeParams) {
final StringBuilder b = new StringBuilder();
for (int i = 0; i < typeParams.length; i++) {
if (i > 0) {
b.append("_");
}
final JClassType typeParam = typeParams[i];
final String pkg = typeParam.getPackage().getName();
for (final String chunk : pkg.split("[.]")) {
b.append(chunk.charAt(0)).append('_');
}
b.append(typeParam.getQualifiedSourceName().replace(pkg+".", "").replace('.', '_'));
}
return b.toString();
}
}