/** * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jboss.errai.ui.rebind; import static org.jboss.errai.codegen.Parameter.finalOf; import static org.jboss.errai.codegen.util.Stmt.castTo; import static org.jboss.errai.codegen.util.Stmt.declareFinalVariable; import static org.jboss.errai.codegen.util.Stmt.invokeStatic; import static org.jboss.errai.codegen.util.Stmt.loadLiteral; import static org.jboss.errai.codegen.util.Stmt.loadVariable; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; import javax.enterprise.context.Dependent; import javax.inject.Named; import org.jboss.errai.codegen.Statement; import org.jboss.errai.codegen.builder.ClassStructureBuilder; import org.jboss.errai.codegen.builder.impl.ObjectBuilder; import org.jboss.errai.codegen.meta.HasAnnotations; import org.jboss.errai.codegen.meta.MetaClass; import org.jboss.errai.codegen.meta.MetaClassFactory; import org.jboss.errai.codegen.meta.MetaMethod; import org.jboss.errai.codegen.util.CDIAnnotationUtils; import org.jboss.errai.common.client.api.annotations.Element; import org.jboss.errai.common.client.api.annotations.Properties; import org.jboss.errai.common.client.api.annotations.Property; import org.jboss.errai.common.client.ui.HasValue; import org.jboss.errai.common.client.ui.NativeHasValueAccessors; import org.jboss.errai.common.client.ui.NativeHasValueAccessors.Accessor; import org.jboss.errai.ioc.client.api.IOCExtension; import org.jboss.errai.ioc.rebind.ioc.bootstrapper.AbstractBodyGenerator; import org.jboss.errai.ioc.rebind.ioc.bootstrapper.FactoryBodyGenerator; import org.jboss.errai.ioc.rebind.ioc.bootstrapper.IOCProcessingContext; import org.jboss.errai.ioc.rebind.ioc.extension.IOCExtensionConfigurator; import org.jboss.errai.ioc.rebind.ioc.graph.api.CustomFactoryInjectable; import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraph; import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.InjectableType; import org.jboss.errai.ioc.rebind.ioc.graph.api.Injectable; import org.jboss.errai.ioc.rebind.ioc.graph.api.InjectionSite; import org.jboss.errai.ioc.rebind.ioc.graph.api.Qualifier; import org.jboss.errai.ioc.rebind.ioc.graph.api.QualifierFactory; import org.jboss.errai.ioc.rebind.ioc.graph.impl.DefaultCustomFactoryInjectable; import org.jboss.errai.ioc.rebind.ioc.graph.impl.FactoryNameGenerator; import org.jboss.errai.ioc.rebind.ioc.graph.impl.InjectableHandle; import org.jboss.errai.ioc.rebind.ioc.injector.api.ExtensionTypeCallback; import org.jboss.errai.ioc.rebind.ioc.injector.api.InjectableProvider; import org.jboss.errai.ioc.rebind.ioc.injector.api.InjectionContext; import org.jboss.errai.ioc.rebind.ioc.injector.api.WiringElementType; import org.jboss.errai.ui.shared.TemplateUtil; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.TagName; import jsinterop.annotations.JsType; /** * Satisfies injection points for DOM elements. * * @author Max Barkley <mbarkley@redhat.com> */ @IOCExtension public class ElementProviderExtension implements IOCExtensionConfigurator { @Override public void configure(final IOCProcessingContext context, final InjectionContext injectionContext) { } @Override public void afterInitialization(final IOCProcessingContext context, final InjectionContext injectionContext) { final MetaClass gwtElement = MetaClassFactory.get(com.google.gwt.dom.client.Element.class); injectionContext.registerExtensionTypeCallback(new ExtensionTypeCallback() { @Override public void callback(final MetaClass type) { try { final Element elementAnno; final JsType jsTypeAnno; if (type.isAssignableTo(gwtElement)) { final TagName gwtTagNameAnno; if ((gwtTagNameAnno = type.getAnnotation(TagName.class)) != null) { processGwtUserElement(injectionContext, type, gwtTagNameAnno); } } else if ((elementAnno = type.getAnnotation(Element.class)) != null) { if ((jsTypeAnno = type.getAnnotation(JsType.class)) == null || !jsTypeAnno.isNative()) { throw new RuntimeException( Element.class.getSimpleName() + " is only valid on native " + JsType.class.getSimpleName() + "s."); } processJsTypeElement(injectionContext, type, elementAnno); } } catch (final Throwable t) { throw new RuntimeException(String.format("Error occurred while processing [%s] in %s.", type.getFullyQualifiedName(), ElementProviderExtension.class.getSimpleName()), t); } } }); } private static void processJsTypeElement(final InjectionContext injectionContext, final MetaClass type, final Element elementAnno) { registerInjectableProvider(injectionContext, type, elementAnno.value()); } private static void processGwtUserElement(final InjectionContext injectionContext, final MetaClass type, final TagName anno) { registerInjectableProvider(injectionContext, type, anno.value()); } private static void registerInjectableProvider(final InjectionContext injectionContext, final MetaClass type, final String... tagNames) { final Set<Property> properties = getProperties(type); for (final String tagName : tagNames) { final Qualifier qualifier = getNamedQualifier(injectionContext.getQualifierFactory(), tagName); final InjectableHandle handle = new InjectableHandle(type, qualifier); injectionContext.registerExactTypeInjectableProvider(handle, new InjectableProvider() { CustomFactoryInjectable injectable; @Override public CustomFactoryInjectable getInjectable(final InjectionSite injectionSite, final FactoryNameGenerator nameGenerator) { if (injectable == null) { final String factoryName = nameGenerator.generateFor(handle.getType(), handle.getQualifier(), InjectableType.ExtensionProvided); final FactoryBodyGenerator generator = new AbstractBodyGenerator() { @Override protected List<Statement> generateCreateInstanceStatements(final ClassStructureBuilder<?> bodyBlockBuilder, final Injectable injectable, final DependencyGraph graph, final InjectionContext injectionContext) { final List<Statement> stmts = new ArrayList<>(); final String elementVar = "element"; stmts.add(declareFinalVariable(elementVar, com.google.gwt.dom.client.Element.class, invokeStatic(Document.class, "get").invoke("createElement", loadLiteral(tagName)))); for (final Property property : properties) { stmts.add(loadVariable(elementVar).invoke("setPropertyString", loadLiteral(property.name()), loadLiteral(property.value()))); } final String retValVar = "retVal"; stmts.add(declareFinalVariable(retValVar, type, invokeStatic(TemplateUtil.class, "nativeCast", loadVariable(elementVar)))); if (implementsNativeHasValueAndRequiresGeneratedInvocation(type)) { stmts.add(invokeStatic(NativeHasValueAccessors.class, "registerAccessor", loadVariable(retValVar), createAccessorImpl(type, retValVar))); } stmts.add(loadVariable(retValVar).returnValue()); return stmts; } }; injectable = new DefaultCustomFactoryInjectable(handle.getType(), handle.getQualifier(), factoryName, Dependent.class, Collections.singletonList(WiringElementType.DependentBean), generator); } return injectable; } }); } } /* * If a type uses @JsOverlay or @JsProperty on overrides of HasValue methods, then we must generate * an invocation so the GWT compiler uses the correct JS invocation at runtime. */ private static boolean implementsNativeHasValueAndRequiresGeneratedInvocation(final MetaClass type) { if (type.isAssignableTo(HasValue.class)) { final MetaClass hasValue = MetaClassFactory.get(HasValue.class); final MetaMethod getValue = type.getMethod("getValue", new MetaClass[0]); final MetaMethod setValue = type.getMethod("setValue", getValue.getReturnType()); if (type.isInterface() && (getValue.getDeclaringClass().getErased().equals(hasValue) || setValue.getDeclaringClass().getErased().equals(hasValue))) { /* * In this case, the methods could be default implementations on an interface (not returned by TypeOracle) so we * will assume we need to generate an invocation. */ return true; } else { final Stream<Annotation> getAnnos = Arrays.stream(getValue.getAnnotations()); final Stream<Annotation> setAnnos = Arrays.stream(setValue.getAnnotations()); final Predicate<Annotation> testForOverlayOrProperty = anno -> anno.annotationType().getPackage().getName().equals("jsinterop.annotations"); return getAnnos.anyMatch(testForOverlayOrProperty) || setAnnos.anyMatch(testForOverlayOrProperty); } } return false; } private static Object createAccessorImpl(final MetaClass type, final String varName) { final MetaClass propertyType = type.getMethod("getValue", new Class[0]).getReturnType(); return ObjectBuilder.newInstanceOf(Accessor.class) .extend() .publicMethod(Object.class, "get") .append(loadVariable(varName).invoke("getValue").returnValue()) .finish() .publicMethod(void.class, "set", finalOf(Object.class, "value")) .append(loadVariable(varName).invoke("setValue", castTo(propertyType, loadVariable("value")))) .finish() .finish(); } private static Set<Property> getProperties(final MetaClass type) { final Set<Property> properties = new HashSet<>(); final Property declaredProperty = type.getAnnotation(Property.class); final Properties declaredProperties = type.getAnnotation(Properties.class); if (declaredProperty != null) { properties.add(declaredProperty); } if (declaredProperties != null) { properties.addAll(Arrays.asList(declaredProperties.value())); } return properties; } private static Qualifier getNamedQualifier(final QualifierFactory factory, final String tagName) { return factory.forSource(new HasAnnotations() { private final Named named = new Named() { @Override public Class<? extends Annotation> annotationType() { return Named.class; } @Override public String value() { return tagName; } @Override public int hashCode() { return CDIAnnotationUtils.hashCode(this); } @Override public String toString() { return CDIAnnotationUtils.toString(this); } @Override public boolean equals(final Object obj) { return obj instanceof Named && CDIAnnotationUtils.equals(this, (Annotation) obj); } }; @Override public boolean isAnnotationPresent(final Class<? extends Annotation> annotation) { return Named.class.equals(annotation); } @Override public Annotation[] getAnnotations() { return new Annotation[] { named }; } @SuppressWarnings("unchecked") @Override public <A extends Annotation> A getAnnotation(final Class<A> annotation) { if (isAnnotationPresent(annotation)) { return (A) named; } else { return null; } } }); } }