/*
* Copyright (C) 2013 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 java.util.Collections.singletonList;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.jboss.errai.codegen.builder.impl.ObjectBuilder.newInstanceOf;
import static org.jboss.errai.codegen.meta.MetaClassFactory.parameterizedAs;
import static org.jboss.errai.codegen.meta.MetaClassFactory.typeParametersOf;
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.declareVariable;
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 static org.jboss.errai.codegen.util.Stmt.nestedCall;
import static org.jboss.errai.codegen.util.Stmt.newObject;
import static org.jboss.errai.ioc.util.GeneratedNamesUtil.qualifiedClassNameToShortenedIdentifier;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.enterprise.util.TypeLiteral;
import org.jboss.errai.codegen.Cast;
import org.jboss.errai.codegen.InnerClass;
import org.jboss.errai.codegen.Parameter;
import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.Variable;
import org.jboss.errai.codegen.builder.AnonymousClassStructureBuilder;
import org.jboss.errai.codegen.builder.BlockBuilder;
import org.jboss.errai.codegen.builder.ClassDefinitionBuilderInterfaces;
import org.jboss.errai.codegen.builder.ClassStructureBuilder;
import org.jboss.errai.codegen.builder.ClassStructureBuilderAbstractMethodOption;
import org.jboss.errai.codegen.builder.ContextualStatementBuilder;
import org.jboss.errai.codegen.builder.impl.ClassBuilder;
import org.jboss.errai.codegen.builder.impl.ObjectBuilder;
import org.jboss.errai.codegen.exception.GenerationException;
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.meta.MetaParameterizedType;
import org.jboss.errai.codegen.meta.MetaType;
import org.jboss.errai.codegen.meta.impl.build.BuildMetaClass;
import org.jboss.errai.codegen.meta.impl.java.JavaReflectionClass;
import org.jboss.errai.codegen.util.Refs;
import org.jboss.errai.codegen.util.Stmt;
import org.jboss.errai.common.client.api.annotations.BrowserEvent;
import org.jboss.errai.common.client.ui.ElementWrapperWidget;
import org.jboss.errai.common.client.ui.HasValue;
import org.jboss.errai.ioc.client.api.CodeDecorator;
import org.jboss.errai.ioc.client.container.DestructionCallback;
import org.jboss.errai.ioc.rebind.ioc.bootstrapper.InjectUtil;
import org.jboss.errai.ioc.rebind.ioc.extension.IOCDecoratorExtension;
import org.jboss.errai.ioc.rebind.ioc.injector.api.Decorable;
import org.jboss.errai.ioc.rebind.ioc.injector.api.FactoryController;
import org.jboss.errai.ui.client.local.spi.TemplateRenderingCallback;
import org.jboss.errai.ui.shared.DataFieldMeta;
import org.jboss.errai.ui.shared.Template;
import org.jboss.errai.ui.shared.TemplateStyleSheet;
import org.jboss.errai.ui.shared.TemplateUtil;
import org.jboss.errai.ui.shared.TemplateWidgetMapper;
import org.jboss.errai.ui.shared.api.annotations.DataField;
import org.jboss.errai.ui.shared.api.annotations.DataField.ConflictStrategy;
import org.jboss.errai.ui.shared.api.annotations.EventHandler;
import org.jboss.errai.ui.shared.api.annotations.ForEvent;
import org.jboss.errai.ui.shared.api.annotations.SinkNative;
import org.jboss.errai.ui.shared.api.annotations.Templated;
import org.jboss.errai.ui.shared.api.style.StyleBindingsRegistry;
import org.lesscss.LessCompiler;
import org.lesscss.LessException;
import org.lesscss.LessSource;
import org.lesscss.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.StyleInjector;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.dom.client.DomEvent.Type;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ClientBundle.Source;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.TextResource;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;
import jsinterop.annotations.JsType;
/**
* Generates the code required for {@link Templated} classes.
*
* @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
* @author Christian Sadilek <csadilek@redhat.com>
*/
//This decorator has to run after the decorator for @DataField
@CodeDecorator(order=1)
public class TemplatedCodeDecorator extends IOCDecoratorExtension<Templated> {
private static final String CONSTRUCTED_TEMPLATE_SET_KEY = "constructedTemplate";
private static final Logger logger = LoggerFactory.getLogger(TemplatedCodeDecorator.class);
public TemplatedCodeDecorator(final Class<Templated> decoratesWith) {
super(decoratesWith);
}
@Override
public void generateDecorator(final Decorable decorable, final FactoryController controller) {
final MetaClass declaringClass = decorable.getDecorableDeclaringType();
final Templated anno = (Templated) decorable.getAnnotation();
final Class<?> templateProvider = anno.provider();
final boolean customProvider = templateProvider != Templated.DEFAULT_PROVIDER.class;
final Optional<String> styleSheetPath = getTemplateStyleSheetPath(declaringClass);
final boolean explicitStyleSheetPresent = styleSheetPath.filter(path -> Thread.currentThread().getContextClassLoader().getResource(path) != null).isPresent();
if (declaringClass.isAssignableTo(Composite.class)) {
logger.warn("The @Templated class, {}, extends Composite. This will not be supported in future versions.", declaringClass.getFullyQualifiedName());
}
if (styleSheetPath.isPresent() && !explicitStyleSheetPresent) {
throw new GenerationException("@Templated class [" + declaringClass.getFullyQualifiedName()
+ "] declared a stylesheet [" + styleSheetPath + "] that could not be found.");
}
final List<Statement> initStmts = new ArrayList<>();
generateTemplatedInitialization(decorable, controller, initStmts, customProvider);
if (customProvider) {
final Statement init =
Stmt.invokeStatic(TemplateUtil.class, "provideTemplate",
templateProvider,
getTemplateUrl(declaringClass),
Stmt.newObject(TemplateRenderingCallback.class)
.extend()
.publicOverridesMethod("renderTemplate", Parameter.of(String.class, "template", true))
.appendAll(initStmts)
.finish()
.finish());
controller.addInitializationStatements(Collections.singletonList(init));
}
else {
controller.addInitializationStatements(initStmts);
}
controller.addDestructionStatements(generateTemplateDestruction(decorable));
controller.addInitializationStatementsToEnd(Collections.<Statement>singletonList(invokeStatic(StyleBindingsRegistry.class, "get")
.invoke("updateStyles", Refs.get("instance"))));
}
/**
* Generates a {@link DestructionCallback} for the {@link Templated} component.
*
* @return statement representing the template destruction logic.
*/
private List<Statement> generateTemplateDestruction(final Decorable decorable) {
final List<Statement> destructionStatements = new ArrayList<>();
final Map<String, Statement> dataFields = DataFieldCodeDecorator.aggregateDataFieldMap(decorable, decorable.getDecorableDeclaringType());
final Map<String, MetaClass> dataFieldTypes =
DataFieldCodeDecorator.aggregateDataFieldTypeMap(decorable, decorable.getDecorableDeclaringType());
for (final String fieldName : dataFields.keySet()) {
final Statement field = dataFields.get(fieldName);
final MetaClass fieldType = dataFieldTypes.get(fieldName);
if (fieldType.isAssignableTo(Element.class)) {
destructionStatements.add(Stmt.invokeStatic(ElementWrapperWidget.class, "removeWidget", field));
}
}
if (decorable.getDecorableDeclaringType().isAssignableTo(Composite.class)) {
destructionStatements.add(Stmt.invokeStatic(TemplateUtil.class, "cleanupWidget", decorable.getAccessStatement()));
} else {
destructionStatements.add(Stmt.invokeStatic(TemplateUtil.class, "cleanupTemplated", decorable.getAccessStatement()));
}
return destructionStatements;
}
/**
* Generate the actual construction logic for our {@link Templated} component
* @param styleSheet
*/
@SuppressWarnings("serial")
private void generateTemplatedInitialization(final Decorable decorable,
final FactoryController controller,
final List<Statement> initStmts,
final boolean customProvider) {
final Map<MetaClass, BuildMetaClass> constructed = getConstructedTemplateTypes(decorable);
final MetaClass declaringClass = decorable.getDecorableDeclaringType();
if (!constructed.containsKey(declaringClass)) {
final String templateVarName = "templateFor" + decorable.getDecorableDeclaringType().getName();
final Optional<String> resolvedStylesheetPath = getResolvedStyleSheetPath(getTemplateStyleSheetPath(declaringClass), declaringClass);
final boolean lessStylesheet = resolvedStylesheetPath.filter(path -> path.endsWith(".less")).isPresent();
/*
* Generate this component's ClientBundle resource if necessary
*/
final boolean generateCssBundle = resolvedStylesheetPath.isPresent() && !lessStylesheet;
if (!customProvider || generateCssBundle) {
generateTemplateResourceInterface(decorable, declaringClass, customProvider, resolvedStylesheetPath.filter(path -> path.endsWith(".css")));
/*
* Instantiate the ClientBundle Template resource
*/
initStmts.add(declareVariable(constructed.get(declaringClass)).named(templateVarName)
.initializeWith(invokeStatic(GWT.class, "create", constructed.get(declaringClass))));
if (generateCssBundle) {
controller.addFactoryInitializationStatements(singletonList(castTo(constructed.get(declaringClass),
invokeStatic(GWT.class, "create", constructed.get(declaringClass))).invoke("getStyle")
.invoke("ensureInjected")));
}
}
/*
* Compile LESS stylesheet to CSS and generate StyleInjector code
*/
if (resolvedStylesheetPath.isPresent() && lessStylesheet) {
try {
final Resource lessResource = new ClassPathResource(resolvedStylesheetPath.get(), Thread.currentThread().getContextClassLoader());
final LessSource source = new LessSource(lessResource);
final LessCompiler compiler = new LessCompiler();
final String compiledCss = compiler.compile(source);
controller.addFactoryInitializationStatements(singletonList(invokeStatic(StyleInjector.class, "inject", loadLiteral(compiledCss))));
} catch (IOException | LessException e) {
throw new RuntimeException("Error while attempting to compile the LESS stylesheet [" + resolvedStylesheetPath.get() + "].", e);
}
}
/*
* Get root Template Element
*/
final String parentOfRootTemplateElementVarName = "parentElementForTemplateOf" + decorable.getDecorableDeclaringType().getName();
initStmts.add(Stmt
.declareVariable(Element.class)
.named(parentOfRootTemplateElementVarName)
.initializeWith(
Stmt.invokeStatic(TemplateUtil.class, "getRootTemplateParentElement",
(customProvider) ? Variable.get("template") :
Stmt.loadVariable(templateVarName).invoke("getContents").invoke("getText"),
getTemplateFileName(declaringClass),
getTemplateFragmentName(declaringClass))));
final Statement rootTemplateElement = Stmt.invokeStatic(TemplateUtil.class, "getRootTemplateElement",
Stmt.loadVariable(parentOfRootTemplateElementVarName));
/*
* If i18n is enabled for this module, translate the root template element here
*/
if (!customProvider) {
translateTemplate(decorable, initStmts, rootTemplateElement);
}
/*
* Get a reference to the actual Composite component being created
*/
final Statement component = Refs.get("instance");
/*
* Get all of the data-field Elements from the Template
*/
final String dataFieldElementsVarName = "dataFieldElements";
initStmts.add(Stmt.declareVariable(dataFieldElementsVarName,
new TypeLiteral<Map<String, Element>>() {},
Stmt.invokeStatic(TemplateUtil.class, "getDataFieldElements",
rootTemplateElement))
);
final String dataFieldMetasVarName = "dataFieldMetas";
initStmts.addAll(generateDataFieldMetas(dataFieldMetasVarName, decorable));
/*
* Attach Widget field children Elements to the Template DOM
*/
final String fieldsMapVarName = "templateFieldsMap";
/*
* The Map<String, Widget> to store actual component field references.
*/
initStmts.add(declareVariable(fieldsMapVarName, new TypeLiteral<Map<String, Widget>>() {},
newObject(new TypeLiteral<LinkedHashMap<String, Widget>>() {}))
);
final Statement fieldsMap = Stmt.loadVariable(fieldsMapVarName);
generateComponentCompositions(decorable, initStmts, component, rootTemplateElement,
loadVariable(dataFieldElementsVarName), fieldsMap, loadVariable(dataFieldMetasVarName));
generateEventHandlerMethodClasses(decorable, controller, initStmts, dataFieldElementsVarName, fieldsMap);
}
}
private Optional<String> getResolvedStyleSheetPath(final Optional<String> declaredStylesheetPath,
final MetaClass declaringClass) {
if (declaredStylesheetPath.isPresent()) {
return declaredStylesheetPath;
}
else {
final String simpleName = declaringClass.getName();
final String unsuffixedPath = declaringClass.getPackageName().replace('.', '/') + "/" + simpleName;
final boolean cssSheetExists = (Thread.currentThread().getContextClassLoader().getResource(unsuffixedPath + ".css") != null);
if (cssSheetExists) {
return Optional.of(unsuffixedPath + ".css");
}
final boolean lessSheetExists = (Thread.currentThread().getContextClassLoader().getResource(unsuffixedPath + ".less") != null);
if (lessSheetExists) {
return Optional.of(unsuffixedPath + ".less");
}
return Optional.empty();
}
}
@SuppressWarnings("serial")
private List<Statement> generateDataFieldMetas(final String dataFieldMetasVarName, final Decorable decorable) {
final Map<String, DataField> annoMap = DataFieldCodeDecorator.aggregateDataFieldAnnotationMap(decorable, decorable.getType());
final List<Statement> stmts = new ArrayList<>(annoMap.size()+1);
stmts.add(declareFinalVariable(dataFieldMetasVarName, new TypeLiteral<Map<String, DataFieldMeta>>() {
}, newObject(parameterizedAs(HashMap.class, typeParametersOf(String.class, DataFieldMeta.class)), annoMap.size())));
annoMap
.entrySet()
.stream()
.map(entry -> {
final String fieldName = entry.getKey();
final DataField dataField = entry.getValue();
Statement dataFieldMetaInstance;
if (dataField.attributeRules().length == 0 && dataField.defaultStrategy().equals(ConflictStrategy.USE_TEMPLATE)) {
dataFieldMetaInstance = newObject(DataFieldMeta.class);
}
else {
dataFieldMetaInstance = newObject(DataFieldMeta.class,
loadLiteral(dataField.attributeRules()), loadLiteral(dataField.defaultStrategy()));
}
return loadVariable(dataFieldMetasVarName).invoke("put", fieldName, dataFieldMetaInstance);
})
.collect(Collectors.toCollection(() -> stmts));
return stmts;
}
private void generateEventHandlerMethodClasses(final Decorable decorable, final FactoryController controller,
final List<Statement> initStmts, final String dataFieldElementsVarName, final Statement fieldsMap) {
final Statement instance = Refs.get("instance");
final Map<String, MetaClass> dataFieldTypes = DataFieldCodeDecorator.aggregateDataFieldTypeMap(decorable, decorable.getDecorableDeclaringType());
dataFieldTypes.put("this", decorable.getDecorableDeclaringType());
final MetaClass declaringClass = decorable.getDecorableDeclaringType();
/* Ensure that no @DataFields are handled more than once when used in combination with @SyncNative */
final Set<String> processedNativeHandlers = new HashSet<>();
final Set<String> processedEventHandlers = new HashSet<>();
for (final MetaMethod method : declaringClass.getMethodsAnnotatedWith(EventHandler.class)) {
final String[] targetDataFieldNames = method.getAnnotation(EventHandler.class).value();
validateNonEmptyEventHandlerTargets(declaringClass, method, targetDataFieldNames);
final MetaClass eventType = assertEventType(declaringClass, method);
if (eventType.isAssignableTo(Event.class)) {
processGwtDomEvent(controller, initStmts, dataFieldElementsVarName, fieldsMap, instance, dataFieldTypes, declaringClass,
processedNativeHandlers, processedEventHandlers, method, targetDataFieldNames, eventType);
}
else if (eventType.isAssignableTo(com.google.web.bindery.event.shared.Event.class)) {
processGwtWidgetEvent(controller, initStmts, fieldsMap, dataFieldTypes, declaringClass, processedNativeHandlers,
processedEventHandlers, method, targetDataFieldNames, eventType);
}
else {
processJsInteropDomEvent(initStmts, dataFieldElementsVarName, fieldsMap, instance, dataFieldTypes, declaringClass, method,
targetDataFieldNames, eventType, controller);
}
}
}
private void processJsInteropDomEvent(final List<Statement> initStmts, final String dataFieldElementsVarName,
final Statement fieldsMap, final Statement instance, final Map<String, MetaClass> dataFieldTypes,
final MetaClass declaringClass, final MetaMethod method, final String[] targetDataFieldNames,
final MetaClass eventType, final FactoryController controller) {
final String[] browserEventTypes = Optional
.ofNullable(method.getParameters()[0].getAnnotation(ForEvent.class))
.map(anno -> anno.value())
.filter(value -> value.length > 0)
.orElseGet(() -> eventType.getAnnotation(BrowserEvent.class).value());
for (final String dataFieldName : targetDataFieldNames) {
final ObjectBuilder listener = ObjectBuilder
.newInstanceOf(org.jboss.errai.common.client.dom.EventListener.class)
.extend()
.publicOverridesMethod("call", Parameter.of(org.jboss.errai.common.client.dom.Event.class, "event"))
.append(InjectUtil.invokePublicOrPrivateMethod(controller, method, castTo(eventType, loadVariable("event"))))
.finish()
.finish();
final ContextualStatementBuilder elementStmt;
if (dataFieldTypes.containsKey(dataFieldName) && !"this".equals(dataFieldName)) {
final MetaClass fieldType = dataFieldTypes.get(dataFieldName);
if (fieldType.isAssignableTo(Widget.class)) {
elementStmt = castTo(Widget.class, nestedCall(fieldsMap).invoke("get", dataFieldName));
}
else {
elementStmt = nestedCall(fieldsMap).invoke("get", dataFieldName);
}
}
else {
elementStmt = loadVariable(dataFieldElementsVarName).invoke("get", dataFieldName);
}
final String listenerVarName = "listenerFor" + eventType.getName() + "Calling" + capitalize(method.getName());
initStmts.add(declareFinalVariable(listenerVarName, org.jboss.errai.common.client.dom.EventListener.class, listener));
for (final String browserEventType : browserEventTypes) {
initStmts.add(invokeStatic(TemplateUtil.class, "setupBrowserEventListener", instance, elementStmt,
loadVariable(listenerVarName), loadLiteral(browserEventType)));
}
}
}
private void processGwtWidgetEvent(final FactoryController controller, final List<Statement> initStmts, final Statement fieldsMap,
final Map<String, MetaClass> dataFieldTypes, final MetaClass declaringClass,
final Set<String> processedNativeHandlers, final Set<String> processedEventHandlers, final MetaMethod method,
final String[] targetDataFieldNames, final MetaClass eventType) {
/*
* We have a GWT Widget type
*/
final MetaClass handlerType = getGwtHandlerType(declaringClass, eventType);
final BlockBuilder<AnonymousClassStructureBuilder> listenerBuiler = ObjectBuilder.newInstanceOf(handlerType)
.extend()
.publicOverridesMethod(handlerType.getMethods()[0].getName(), Parameter.of(eventType, "event"));
listenerBuiler.append(InjectUtil.invokePublicOrPrivateMethod(controller, method, Stmt.loadVariable("event")));
final ObjectBuilder listenerInstance = listenerBuiler.finish().finish();
final MetaClass hasHandlerType = MetaClassFactory.get("com.google.gwt.event.dom.client.Has"
+ handlerType.getName()
+ "s");
for (final String name : targetDataFieldNames) {
final MetaClass dataFieldType = dataFieldTypes.get(name);
if (dataFieldType == null) {
throw new GenerationException("@EventHandler method [" + method.getName() + "] in class ["
+ declaringClass.getFullyQualifiedName()
+ "] handles a GWT event type but the specified @DataField [" + name + "] was not found.");
}
if (processedNativeHandlers.contains(name)) {
throw new GenerationException(
"Cannot specify more than one @EventHandler method when @SinkNative is used for data-field ["
+ name + "] in class [" + declaringClass.getFullyQualifiedName()
+ "].");
}
processedEventHandlers.add(name);
// Where will the event come from? It could be a @DataField member, or it could be the templated widget itself!
final Statement eventSource;
if ("this".equals(name)) {
eventSource = Stmt.loadVariable("instance");
}
else {
eventSource = Stmt.nestedCall(fieldsMap).invoke("get", name);
}
if (dataFieldType.isAssignableTo(Element.class)) {
initStmts.add(Stmt.invokeStatic(TemplateUtil.class, "setupWrappedElementEventHandler",
eventSource, listenerInstance,
Stmt.invokeStatic(eventType, "getType")));
}
else if (dataFieldType.isAssignableTo(hasHandlerType)) {
final Statement widget = Cast.to(hasHandlerType, eventSource);
initStmts.add(Stmt.nestedCall(widget).invoke("add" + handlerType.getName(),
Cast.to(handlerType, listenerInstance)));
}
else if (dataFieldType.isAssignableTo(Widget.class)) {
final Statement widget = Cast.to(Widget.class, eventSource);
initStmts.add(Stmt.nestedCall(widget).invoke("addDomHandler",
listenerInstance, Stmt.invokeStatic(eventType, "getType")));
} else if (RebindUtil.isNativeJsType(dataFieldType) || RebindUtil.isElementalIface(dataFieldType)) {
initStmts.add(Stmt.invokeStatic(TemplateUtil.class, "setupWrappedElementEventHandler",
eventSource, listenerInstance,
Stmt.invokeStatic(eventType, "getType")));
} else if (dataFieldType.isAnnotationPresent(Templated.class)) {
final ContextualStatementBuilder widget = Stmt.invokeStatic(TemplateWidgetMapper.class, "get", eventSource);
initStmts.add(widget.invoke("addDomHandler",
listenerInstance, Stmt.invokeStatic(eventType, "getType")));
} else {
throw new GenerationException("@DataField [" + name + "] of type [" + dataFieldType.getName()
+ "] in class [" + declaringClass.getFullyQualifiedName()
+ "] must implement the interface [" + hasHandlerType.getName()
+ "] specified by @EventHandler method " + method.getName() + "(" + eventType.getName()
+ ")], be a DOM element (wrapped as either a JavaScriptObject or a native @JsType), "
+ "or be a @Templated bean.");
}
}
}
private MetaClass getGwtHandlerType(final MetaClass declaringClass, final MetaClass eventType) {
try {
return getHandlerForEvent(eventType);
}
catch (final GenerationException e) {
/*
* see ERRAI-373 for details on this crazy inference (without this message, the cause of the
* problem is nearly impossible to diagnose)
*/
if (declaringClass.getClass() == JavaReflectionClass.class) {
throw new GenerationException(
"The type " + declaringClass.getFullyQualifiedName() + " looks like a client-side" +
" @Templated class, but it is not known to GWT. This probably means that " +
declaringClass.getName() + " or one of its supertypes contains non-translatable code." +
" Run the GWT compiler with logLevel=DEBUG to pinpoint the problem.", e);
}
throw e;
}
}
private void processGwtDomEvent(final FactoryController controller, final List<Statement> initStmts,
final String dataFieldElementsVarName, final Statement fieldsMap, final Statement instance,
final Map<String, MetaClass> dataFieldTypes, final MetaClass declaringClass,
final Set<String> processedNativeHandlers, final Set<String> processedEventHandlers, final MetaMethod method,
final String[] targetDataFieldNames, final MetaClass eventType) {
/*
* Generate native DOM event handlers.
*/
final MetaClass handlerType = MetaClassFactory.get(EventListener.class);
final BlockBuilder<AnonymousClassStructureBuilder> listenerBuilder = ObjectBuilder.newInstanceOf(handlerType)
.extend()
.publicOverridesMethod(handlerType.getMethods()[0].getName(), Parameter.of(eventType, "event"));
listenerBuilder.append(InjectUtil.invokePublicOrPrivateMethod(controller, method, Stmt.loadVariable("event")));
final ObjectBuilder listenerInstance = listenerBuilder.finish().finish();
int eventsToSink =
Event.FOCUSEVENTS | Event.GESTUREEVENTS | Event.KEYEVENTS | Event.MOUSEEVENTS | Event.TOUCHEVENTS;
if (method.isAnnotationPresent(SinkNative.class)) {
eventsToSink = method.getAnnotation(SinkNative.class).value();
}
for (final String name : targetDataFieldNames) {
if (processedNativeHandlers.contains(name) || processedEventHandlers.contains(name)) {
throw new GenerationException(
"Cannot specify more than one @EventHandler method when @SyncNative is used for data-field ["
+ name + "] in class ["
+ declaringClass.getFullyQualifiedName()
+ "].");
}
else {
processedNativeHandlers.add(name);
}
final ContextualStatementBuilder elementStmt;
if (dataFieldTypes.containsKey(name)) {
final MetaClass dataFieldType = dataFieldTypes.get(name);
final boolean gwtUserElement = dataFieldType.isAssignableTo(Element.class);
final boolean nativeJsType = RebindUtil.isNativeJsType(dataFieldType);
if (gwtUserElement || nativeJsType) {
if (dataFieldType.isAssignableTo(HasValue.class)) {
final MetaClass valueType = dataFieldType.getMethod("getValue", new Class[0]).getReturnType();
elementStmt = Stmt.castTo(ElementWrapperWidget.class, Stmt.nestedCall(fieldsMap).invoke("get", name, loadLiteral(valueType)));
}
else {
elementStmt = Stmt.castTo(ElementWrapperWidget.class, Stmt.nestedCall(fieldsMap).invoke("get", name));
}
} else {
/*
* We have a GWT or other Widget type.
*/
throw new GenerationException("@DataField [" + name + "] of type [" + dataFieldType.getName()
+ "] in class [" + declaringClass.getFullyQualifiedName() + "] is not assignable to ["
+ Element.class.getName() + "] specified by @EventHandler method " + method.getName()
+ "("
+ eventType.getName() + ")]\n");
}
} else {
elementStmt = Stmt.loadVariable(dataFieldElementsVarName).invoke("get", name);
}
initStmts.add(Stmt.invokeStatic(TemplateUtil.class, "setupNativeEventListener", instance,
elementStmt, listenerInstance,
eventsToSink));
}
}
private void validateNonEmptyEventHandlerTargets(final MetaClass declaringClass, final MetaMethod method, final String[] targetDataFieldNames) {
if (targetDataFieldNames.length == 0) {
throw new GenerationException("@EventHandler annotation on method ["
+ declaringClass.getFullyQualifiedName()
+ "." + method.getName() + "] must specify at least one data-field target.");
}
}
private MetaClass assertEventType(final MetaClass declaringClass, final MetaMethod method) {
final MetaClass eventType = (method.getParameters().length == 1) ? method.getParameters()[0].getType() : null;
if (eventType != null) {
if (eventType.isAssignableTo(Event.class) || eventType.isAssignableTo(DomEvent.class)) {
return eventType;
}
else if (eventType.isAnnotationPresent(BrowserEvent.class) && Optional
.ofNullable(eventType.getAnnotation(JsType.class)).filter(anno -> anno.isNative()).isPresent()) {
final BrowserEvent eventTypeAnno = eventType.getAnnotation(BrowserEvent.class);
final boolean eventTypeMatchesAll = eventTypeAnno.value().length == 0;
final Optional<ForEvent> oParamAnno = Optional.ofNullable(method.getParameters()[0].getAnnotation(ForEvent.class)).filter(anno -> anno.value().length > 0);
final boolean parameterDeclaresEvent = oParamAnno.isPresent();
if (eventTypeMatchesAll && parameterDeclaresEvent
|| !eventTypeMatchesAll && (!parameterDeclaresEvent || Arrays.asList(eventTypeAnno.value()).containsAll(Arrays.asList(oParamAnno.get().value())))) {
return eventType;
}
else {
String message = String.format("@EventHandler parameter [%s] of method [%s] in class [%s] must declare an event type with @%s",
method.getParameters()[0].getName(), method.getName(), declaringClass.getFullyQualifiedName(), ForEvent.class.getSimpleName());
if (!eventTypeMatchesAll) {
message += " and must be a subset of the following event types: " + Arrays.toString(eventTypeAnno.value());
}
throw new GenerationException(message);
}
}
}
throw new GenerationException(String.format(
"@EventHandler method [%s] in class [%s] must have exactly one parameter of a type "
+ "annotated with @%s and @JsType(isNative=true) or extending either [%s] or [%s].",
method.getName(), declaringClass.getFullyQualifiedName(), BrowserEvent.class.getSimpleName(),
DomEvent.class.getName(), NativeEvent.class.getName()));
}
private MetaClass getHandlerForEvent(final MetaClass eventType) {
/*
* All handlers event must have an overrided method getAssociatedType(). We
* take advantage of this information to get the associated handler. Ex:
* com.google.gwt.event.dom.client.ClickEvent --->
* com.google.gwt.event.dom.client.ClickHandler
*
* com.google.gwt.event.dom.client.BlurEvent --->
* com.google.gwt.event.dom.client.BlurHandler
*/
if (eventType == null) {
return null;
}
MetaMethod method = eventType.getBestMatchingMethod("getAssociatedType", Type.class);
if (method == null) {
for (final MetaMethod m : eventType.getMethods()) {
if ("getAssociatedType".equals(m.getName())) {
method = m;
break;
}
}
}
if (method == null) {
throw new GenerationException("Method 'getAssociatedType()' could not be found in the event ["
+ eventType.getName() + "]");
}
final MetaType returnType = method.getGenericReturnType();
if (returnType == null) {
throw new GenerationException("The method 'getAssociatedType()' in the event [" + eventType.getName()
+ "] returns void.");
}
logger.debug("eventType: " + eventType.getClass() + " -- " + eventType);
logger.debug("method: " + method.getClass() + " -- " + method);
logger.debug("genericReturnType: " + returnType.getClass() + " -- " + returnType);
if (!(returnType instanceof MetaParameterizedType)) {
throw new GenerationException("The method 'getAssociatedType()' in the event [" + eventType.getName()
+ "] does not return Type<? extends EventHandler>..");
}
final MetaParameterizedType parameterizedType = (MetaParameterizedType) returnType;
logger.debug("parameterizedType: " + parameterizedType.getClass() + " -- " + parameterizedType);
final MetaType[] argTypes = parameterizedType.getTypeParameters();
if ((argTypes.length != 1) && argTypes[0] instanceof MetaClass
&& !((MetaClass) argTypes[0]).isAssignableTo(EventHandler.class)) {
throw new GenerationException("The method 'getAssociatedType()' in the event [" + eventType.getName()
+ "] does not return Type<? extends EventHandler>..");
}
return (MetaClass) argTypes[0];
}
/**
* Translates the template using the module's i18n message bundle (only if
* i18n is enabled for the module).
* @param decorable
* @param initStmts
* @param rootTemplateElement
*/
private void translateTemplate(final Decorable decorable, final List<Statement> initStmts,
final Statement rootTemplateElement) {
initStmts.add(
Stmt.invokeStatic(
TemplateUtil.class,
"translateTemplate",
getTemplateFileName(decorable.getDecorableDeclaringType()),
rootTemplateElement
));
}
private void generateComponentCompositions(final Decorable decorable,
final List<Statement> initStmts,
final Statement component,
final Statement rootTemplateElement,
final Statement dataFieldElements,
final Statement fieldsMap,
final Statement fieldsMetaMap) {
final boolean composite = decorable.getEnclosingInjectable().getInjectedType().isAssignableTo(Composite.class);
/*
* Merge each field's Widget Element into the DOM in place of the
* corresponding data-field
*/
final Map<String, Statement> dataFields = DataFieldCodeDecorator.aggregateDataFieldMap(decorable, decorable.getEnclosingInjectable().getInjectedType());
for (final Entry<String, Statement> field : dataFields.entrySet()) {
initStmts.add(invokeStatic(TemplateUtil.class, "compositeComponentReplace", decorable.getDecorableDeclaringType()
.getFullyQualifiedName(), getTemplateFileName(decorable.getDecorableDeclaringType()), supplierOf(Cast.to(Widget.class, field.getValue())),
dataFieldElements, fieldsMetaMap, field.getKey()));
}
/*
* Add each field to the Collection of children of the new Composite
* Template
*/
for (final Entry<String, Statement> field : dataFields.entrySet()) {
initStmts.add(Stmt.nestedCall(fieldsMap).invoke("put", field.getKey(), field.getValue()));
}
final String initMethodName;
if (composite) {
/*
* Attach the Template to the Component, and set up the GWT Widget hierarchy
* to preserve Handlers and DOM events.
*/
initMethodName = "initWidget";
} else {
initMethodName = "initTemplated";
}
initStmts.add(Stmt.invokeStatic(TemplateUtil.class, initMethodName, component, rootTemplateElement,
Stmt.nestedCall(fieldsMap).invoke("values")));
}
private static Statement supplierOf(final Statement value) {
return newInstanceOf(parameterizedAs(Supplier.class, typeParametersOf(value.getType())))
.extend()
.publicOverridesMethod("get")
.append(Stmt.nestedCall(value).returnValue())
.finish()
.finish();
}
/**
* Possibly create an inner interface {@link ClientBundle} for the template's HTML or CSS resources.
* @param customProvider
* @param generateCssBundle
*/
private void generateTemplateResourceInterface(final Decorable decorable, final MetaClass type, final boolean customProvider, final Optional<String> cssPath) {
final ClassDefinitionBuilderInterfaces<ClassStructureBuilderAbstractMethodOption> ifaceDef = ClassBuilder
.define(getTemplateTypeName(type)).publicScope().interfaceDefinition();
if (!customProvider) {
ifaceDef.implementsInterface(Template.class);
}
if (cssPath.isPresent()) {
ifaceDef.implementsInterface(TemplateStyleSheet.class);
}
final ClassStructureBuilder<ClassStructureBuilderAbstractMethodOption> componentTemplateResource = ifaceDef
.implementsInterface(ClientBundle.class).body();
if (!customProvider) {
componentTemplateResource.publicMethod(TextResource.class, "getContents").annotatedWith(new Source() {
@Override
public Class<? extends Annotation> annotationType() {
return Source.class;
}
@Override
public String[] value() {
return new String[] { getTemplateFileName(type) };
}
}).finish();
}
cssPath.ifPresent(path -> addCssResourceMethod(componentTemplateResource, path));
decorable.getFactoryMetaClass().addInnerClass(new InnerClass(componentTemplateResource.getClassDefinition()));
getConstructedTemplateTypes(decorable).put(type, componentTemplateResource.getClassDefinition());
}
private void addCssResourceMethod(
final ClassStructureBuilder<ClassStructureBuilderAbstractMethodOption> componentTemplateResource,
final String templateStyleSheetPath) {
componentTemplateResource.publicMethod(CssResource.class, "getStyle").annotatedWith(new Source() {
@Override
public Class<? extends Annotation> annotationType() {
return Source.class;
}
@Override
public String[] value() {
return new String[] { templateStyleSheetPath };
}
}, new CssResource.NotStrict() {
@Override
public Class<? extends Annotation> annotationType() {
return CssResource.NotStrict.class;
}
}).finish();
}
public static Optional<String> getTemplateStyleSheetPath(final MetaClass type) {
final Templated anno = type.getAnnotation(Templated.class);
if (anno.stylesheet().isEmpty()) {
return Optional.empty();
}
else {
final String rawPath = anno.stylesheet();
final boolean absolute = rawPath.startsWith("/");
if (absolute) {
return Optional.of(rawPath.substring(1));
} else {
return Optional.of(type.getPackageName().replace('.', '/') + "/" + rawPath);
}
}
}
/**
* Get a map of all previously constructed {@link Template} object types
*/
@SuppressWarnings("unchecked")
private Map<MetaClass, BuildMetaClass> getConstructedTemplateTypes(final Decorable decorable) {
Map<MetaClass, BuildMetaClass> result = (Map<MetaClass, BuildMetaClass>) decorable.getInjectionContext().getAttribute(
CONSTRUCTED_TEMPLATE_SET_KEY);
if (result == null) {
result = new LinkedHashMap<>();
decorable.getInjectionContext().setAttribute(CONSTRUCTED_TEMPLATE_SET_KEY, result);
}
return result;
}
/*
* Non-generation utility methods.
*/
/**
* Get the name of the {@link Template} class of the given {@link MetaClass} type
*/
private String getTemplateTypeName(final MetaClass type) {
return qualifiedClassNameToShortenedIdentifier(type) + "TemplateResource";
}
/**
* Get the name of the {@link Template} HTML file of the given {@link MetaClass} component type
*/
public static String getTemplateFileName(final MetaClass type) {
String resource = type.getFullyQualifiedName().replace('.', '/') + ".html";
if (type.isAnnotationPresent(Templated.class)) {
final String source = canonicalizeTemplateSourceSyntax(type, type.getAnnotation(Templated.class).value());
final Matcher matcher = Pattern.compile("^([^#]+)#?.*$").matcher(source);
if (matcher.matches()) {
resource = (matcher.group(1) == null ? resource : matcher.group(1));
if (resource.matches("\\S+\\.html")) {
if (resource.startsWith("/")) {
resource = resource.substring(1);
}
else {
resource = type.getPackageName().replace('.', '/') + "/" + resource;
}
}
}
}
return resource;
}
/**
* Get the URL of the server-side {@link Template} HTML file of the given {@link MetaClass} component type
*/
public static String getTemplateUrl(final MetaClass type) {
String resource = type.getFullyQualifiedName().replace('.', '/') + ".html";
if (type.isAnnotationPresent(Templated.class)) {
final String source = canonicalizeTemplateSourceSyntax(type, type.getAnnotation(Templated.class).value());
final Matcher matcher = Pattern.compile("^([^#]+)#?.*$").matcher(source);
if (matcher.matches()) {
resource = (matcher.group(1) == null ? resource : matcher.group(1));
}
}
return resource;
}
/**
* Get the name of the {@link Template} HTML fragment (Element subtree) to be used as the template root of the given
* {@link MetaClass} component type
*/
public static String getTemplateFragmentName(final MetaClass type) {
String fragment = "";
if (type.isAnnotationPresent(Templated.class)) {
final String source = canonicalizeTemplateSourceSyntax(type, type.getAnnotation(Templated.class).value());
final Matcher matcher = Pattern.compile("^.*#([^#]+)$").matcher(source);
if (matcher.matches()) {
fragment = (matcher.group(1) == null ? fragment : matcher.group(1));
}
}
return fragment;
}
/**
* Throw an exception if the template source syntax is invalid
*/
private static String canonicalizeTemplateSourceSyntax(final MetaClass component, final String source) {
final String result = Strings.nullToEmpty(source).trim();
if (result.matches(".*#.*#.*")) {
throw new IllegalArgumentException("Invalid syntax: @" + Templated.class.getSimpleName() + "(" + source
+ ") on component " + component.getFullyQualifiedName()
+ ". Multiple '#' found, where only one fragment is permitted.");
}
return result;
}
}