/*
* Copyright (C) 2011 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.databinding.rebind;
import static org.jboss.errai.codegen.Parameter.finalOf;
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.loadLiteral;
import static org.jboss.errai.codegen.util.Stmt.loadVariable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jboss.errai.codegen.BlockStatement;
import org.jboss.errai.codegen.Cast;
import org.jboss.errai.codegen.Context;
import org.jboss.errai.codegen.DefParameters;
import org.jboss.errai.codegen.Parameter;
import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.Variable;
import org.jboss.errai.codegen.builder.BlockBuilder;
import org.jboss.errai.codegen.builder.ClassStructureBuilder;
import org.jboss.errai.codegen.builder.ContextualStatementBuilder;
import org.jboss.errai.codegen.builder.ElseBlockBuilder;
import org.jboss.errai.codegen.builder.impl.ClassBuilder;
import org.jboss.errai.codegen.builder.impl.ObjectBuilder;
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.MetaParameter;
import org.jboss.errai.codegen.meta.MetaParameterizedType;
import org.jboss.errai.codegen.meta.MetaType;
import org.jboss.errai.codegen.meta.MetaTypeVariable;
import org.jboss.errai.codegen.util.Bool;
import org.jboss.errai.codegen.util.EmptyStatement;
import org.jboss.errai.codegen.util.If;
import org.jboss.errai.codegen.util.Refs;
import org.jboss.errai.codegen.util.Stmt;
import org.jboss.errai.databinding.client.BindableProxy;
import org.jboss.errai.databinding.client.BindableProxyAgent;
import org.jboss.errai.databinding.client.BindableProxyFactory;
import org.jboss.errai.databinding.client.HasProperties;
import org.jboss.errai.databinding.client.NonExistingPropertyException;
import org.jboss.errai.databinding.client.PropertyType;
import org.jboss.errai.databinding.client.api.Bindable;
import org.jboss.errai.databinding.client.api.StateSync;
import org.jboss.errai.databinding.client.api.handler.property.PropertyChangeEvent;
import org.jboss.errai.databinding.client.api.handler.property.PropertyChangeHandler;
import com.google.gwt.core.ext.TreeLogger;
/**
* Generates a proxy for a {@link Bindable} type. A bindable proxy subclasses the bindable type and
* overrides all non-final methods to trigger UI updates and fire property change events when
* required.
*
* @author Christian Sadilek <csadilek@redhat.com>
*/
public class BindableProxyGenerator {
private final MetaClass bindable;
private final String agentField;
private final TreeLogger logger;
private final Set<MetaMethod> proxiedAccessorMethods;
public BindableProxyGenerator(final MetaClass bindable, final TreeLogger logger) {
this.bindable = bindable;
this.agentField = inferSafeAgentFieldName();
this.logger = logger;
this.proxiedAccessorMethods = new HashSet<>();
}
public ClassStructureBuilder<?> generate() {
final String safeProxyClassName = bindable.getFullyQualifiedName().replace('.', '_') + "Proxy";
final ClassStructureBuilder<?> classBuilder = ClassBuilder.define(safeProxyClassName, bindable)
.packageScope()
.implementsInterface(BindableProxy.class)
.body();
classBuilder
.privateField(agentField, parameterizedAs(BindableProxyAgent.class, typeParametersOf(bindable)))
.finish()
.publicConstructor()
.callThis(Stmt.newObject(bindable))
.finish()
.publicConstructor(Parameter.of(bindable, "target"))
.append(Stmt.loadVariable(agentField).assignValue(
Stmt.newObject(parameterizedAs(BindableProxyAgent.class, typeParametersOf(bindable)),
Variable.get("this"), Variable.get("target"))))
.append(generatePropertiesMap())
.append(agent().invoke("copyValues"))
.appendAll(registerDeclarativeHandlers(bindable))
.finish()
.publicMethod(BindableProxyAgent.class, "getBindableProxyAgent")
.append(agent().returnValue())
.finish()
.publicMethod(void.class, "updateWidgets")
.append(agent().invoke("updateWidgetsAndFireEvents"))
.finish()
.publicMethod(bindable, "unwrap")
.append(target().returnValue())
.finish()
.publicMethod(bindable, "deepUnwrap")
.append(generateDeepUnwrapMethodBody("deepUnwrap"))
.finish()
.publicMethod(boolean.class, "equals", Parameter.of(Object.class, "obj"))
.append(
If.instanceOf(Variable.get("obj"), classBuilder.getClassDefinition())
.append(Stmt.loadVariable("obj").assignValue(
Stmt.castTo(classBuilder.getClassDefinition(), Variable.get("obj")).invoke("unwrap")))
.finish())
.append(target().invoke("equals", Variable.get("obj")).returnValue())
.finish()
.publicMethod(int.class, "hashCode")
.append(target().invoke("hashCode").returnValue())
.finish()
.publicMethod(String.class, "toString")
.append(target().invoke("toString").returnValue())
.finish();
generateAccessorMethods(classBuilder);
generateNonAccessorMethods(classBuilder);
return classBuilder;
}
private Collection<Statement> registerDeclarativeHandlers(final MetaClass bindable) {
final List<MetaMethod> handlerMethods = bindable.getMethodsAnnotatedWith(org.jboss.errai.ui.shared.api.annotations.PropertyChangeHandler.class);
if ( handlerMethods.isEmpty() ) return Collections.emptyList();
final List<Statement> retVal = new ArrayList<>();
for (final MetaMethod method : handlerMethods) {
if (method.getParameters().length == 1
&& method.getParameters()[0].getType().getFullyQualifiedName().equals(PropertyChangeEvent.class.getName())) {
final String property = method.getAnnotation(org.jboss.errai.ui.shared.api.annotations.PropertyChangeHandler.class).value();
if (!property.isEmpty()) validateProperty(bindable, property);
final Object handler = createHandlerForMethod(method);
final ContextualStatementBuilder subStmt = (property.isEmpty() ?
loadVariable("agent").invoke("addPropertyChangeHandler", handler):
loadVariable("agent").invoke("addPropertyChangeHandler", property, handler));
retVal.add(subStmt);
}
else {
throw new RuntimeException(
String.format("The @ChangeHandler method [%s] must have exactly one argument of type %s.",
method.getName(), PropertyChangeEvent.class.getSimpleName()));
}
}
return retVal;
}
private void validateProperty(final MetaClass bindable, final String property) {
if (!bindable.getBeanDescriptor().getProperties().contains(property)) {
throw new RuntimeException(String.format("Invalid property name [%s] in @Bindable type [%s].", property,
bindable.getFullyQualifiedName()));
}
}
private Object createHandlerForMethod(final MetaMethod method) {
return ObjectBuilder
.newInstanceOf(PropertyChangeHandler.class)
.extend()
.publicOverridesMethod("onPropertyChange", finalOf(PropertyChangeEvent.class, "event"))
.append(castTo(method.getDeclaringClass(), loadVariable("agent").loadField("target")).invoke(method, loadVariable("event")))
.finish()
.finish();
}
/**
* Generates accessor methods for all Java bean properties plus the corresponding code for the
* method implementations of {@link HasProperties}.
*/
private void generateAccessorMethods(final ClassStructureBuilder<?> classBuilder) {
final BlockBuilder<?> getMethod = classBuilder.publicMethod(Object.class, "get",
Parameter.of(String.class, "property"));
final BlockBuilder<?> setMethod = classBuilder.publicMethod(void.class, "set",
Parameter.of(String.class, "property"),
Parameter.of(Object.class, "value"));
for (final String property : bindable.getBeanDescriptor().getProperties()) {
generateGetter(classBuilder, property, getMethod);
generateSetter(classBuilder, property, setMethod);
}
getMethod.append(
If.objEquals(Stmt.loadVariable("property"), "this")
.append(target().returnValue())
.finish()
);
setMethod.append(
If.cond(Stmt.loadVariable("property").invoke("equals", "this"))
.append(agent().loadField("target").assignValue(
Stmt.castTo(bindable, Stmt.loadVariable("value"))))
.append(
Stmt.returnVoid())
.finish()
);
final Statement nonExistingPropertyException = Stmt.throw_(NonExistingPropertyException.class,
Stmt.loadLiteral(bindable.getName()), Variable.get("property"));
getMethod.append(nonExistingPropertyException).finish();
setMethod.append(nonExistingPropertyException).finish();
classBuilder.publicMethod(Map.class, "getBeanProperties")
.append(Stmt.declareFinalVariable("props", Map.class, ObjectBuilder.newInstanceOf(HashMap.class).withParameters(agent().loadField("propertyTypes"))))
.append(Stmt.loadVariable("props").invoke("remove", "this"))
.append(Stmt.invokeStatic(Collections.class, "unmodifiableMap", Stmt.loadVariable("props")).returnValue())
.finish();
}
/**
* Generates a getter method for the provided property plus the corresponding code for the
* implementation of {@link HasProperties#get(String)}.
*/
private void generateGetter(final ClassStructureBuilder<?> classBuilder, final String property,
final BlockBuilder<?> getMethod) {
final MetaMethod getterMethod = bindable.getBeanDescriptor().getReadMethodForProperty(property);
if (getterMethod != null && !getterMethod.isFinal()) {
getMethod.append(
If.objEquals(Stmt.loadVariable("property"), property)
.append(Stmt.loadVariable("this").invoke(getterMethod.getName()).returnValue())
.finish()
);
classBuilder.publicMethod(getterMethod.getReturnType(), getterMethod.getName())
.append(target().invoke(getterMethod.getName()).returnValue())
.finish();
proxiedAccessorMethods.add(getterMethod);
}
}
/**
* Generates a setter method for the provided property plus the corresponding code for the
* implementation of {@link HasProperties#set(String, Object)}.
*/
private void generateSetter(final ClassStructureBuilder<?> classBuilder, final String property, final BlockBuilder<?> setMethod) {
final MetaMethod getterMethod = bindable.getBeanDescriptor().getReadMethodForProperty(property);
final MetaMethod setterMethod = bindable.getBeanDescriptor().getWriteMethodForProperty(property);
if (getterMethod != null && setterMethod != null && !setterMethod.isFinal()) {
setMethod.append(
If.cond(Stmt.loadVariable("property").invoke("equals", property))
.append(
target().invoke(setterMethod.getName(),
Cast.to(setterMethod.getParameters()[0].getType().asBoxed(), Variable.get("value"))))
.append(
Stmt.returnVoid())
.finish()
);
final MetaClass paramType = setterMethod.getParameters()[0].getType();
// If the setter method we are proxying returns a value, capture that value into a local variable
Statement returnValueOfSetter = null;
final String returnValName = ensureSafeLocalVariableName("returnValueOfSetter", setterMethod);
Statement wrappedListProperty = EmptyStatement.INSTANCE;
if (paramType.isAssignableTo(List.class)) {
wrappedListProperty = Stmt.loadVariable(property).assignValue(
Cast.to(paramType ,agent().invoke("ensureBoundListIsProxied", property, Stmt.loadVariable(property))));
}
Statement callSetterOnTarget =
target().invoke(setterMethod.getName(), Cast.to(paramType, Stmt.loadVariable(property)));
if (!setterMethod.getReturnType().equals(MetaClassFactory.get(void.class))) {
callSetterOnTarget =
Stmt.declareFinalVariable(returnValName, setterMethod.getReturnType(), callSetterOnTarget);
returnValueOfSetter = Stmt.nestedCall(Refs.get(returnValName)).returnValue();
}
else {
returnValueOfSetter = EmptyStatement.INSTANCE;
}
Statement updateNestedProxy = null;
if (DataBindingUtil.isBindableType(paramType)) {
updateNestedProxy =
Stmt.if_(Bool.expr(agent("binders").invoke("containsKey", property)))
.append(Stmt.loadVariable(property).assignValue(Cast.to(paramType,
agent("binders").invoke("get", property).invoke("setModel", Variable.get(property),
Stmt.loadStatic(StateSync.class, "FROM_MODEL"),
Stmt.loadLiteral(true)))))
.finish();
}
else {
updateNestedProxy = EmptyStatement.INSTANCE;
}
final String oldValName = ensureSafeLocalVariableName("oldValue", setterMethod);
final boolean propertyIsList = bindable.getBeanDescriptor().getPropertyType(property).getFullyQualifiedName().equals(List.class.getName());
classBuilder.publicMethod(setterMethod.getReturnType(), setterMethod.getName(),
Parameter.of(paramType, property))
.append(updateNestedProxy)
.append(
Stmt.declareVariable(oldValName, paramType, target().invoke(getterMethod.getName())))
.append(wrappedListProperty)
.append(callSetterOnTarget)
.append(
agent().invoke("updateWidgetsAndFireEvent", propertyIsList, property, Variable.get(oldValName), Variable.get(property)))
.append(returnValueOfSetter)
.finish();
proxiedAccessorMethods.add(setterMethod);
}
}
/**
* Generates proxy methods overriding public non-final methods that are not also property accessor
* methods. The purpose of this is to allow the proxies to react on model changes that happen
* outside setters of the bean. These methods will cause a comparison of all bound properties and
* trigger the appropriate UI updates and property change events.
*/
private void generateNonAccessorMethods(final ClassStructureBuilder<?> classBuilder) {
for (final MetaMethod method : bindable.getMethods()) {
final String methodName = method.getName();
if (!proxiedAccessorMethods.contains(method)
&& !methodName.equals("hashCode") && !methodName.equals("equals") && !methodName.equals("toString")
&& method.isPublic() && !method.isFinal() && !method.isStatic()) {
final Parameter[] parms = DefParameters.from(method).getParameters().toArray(new Parameter[0]);
final List<Statement> parmVars = new ArrayList<>();
for (int i = 0; i < parms.length; i++) {
parmVars.add(Stmt.loadVariable(parms[i].getName()));
final MetaClass type = getTypeOrFirstUpperBound(method.getGenericParameterTypes()[i], method);
if (type == null) return;
parms[i] = Parameter.of(type, parms[i].getName());
}
Statement callOnTarget = null;
Statement returnValue = null;
final String returnValName = ensureSafeLocalVariableName("returnValue", method);
final MetaClass returnType = getTypeOrFirstUpperBound(method.getGenericReturnType(), method);
if (returnType == null)
return;
if (!returnType.equals(MetaClassFactory.get(void.class))) {
callOnTarget = Stmt.declareFinalVariable(returnValName,
returnType, target().invoke(method, parmVars.toArray()));
returnValue = Stmt.nestedCall(Refs.get(returnValName)).returnValue();
}
else {
callOnTarget = target().invoke(method, parmVars.toArray());
returnValue = EmptyStatement.INSTANCE;
}
classBuilder
.publicMethod(returnType, methodName, parms)
.append(callOnTarget)
.append(agent().invoke("updateWidgetsAndFireEvents"))
.append(returnValue)
.finish();
}
}
}
/**
* Generates code to collect all existing properties and their types.
*/
private Statement generatePropertiesMap() {
final BlockStatement block = new BlockStatement();
for (final String property : bindable.getBeanDescriptor().getProperties()) {
final MetaMethod readMethod = bindable.getBeanDescriptor().getReadMethodForProperty(property);
if (readMethod != null && !readMethod.isFinal()) {
final MetaClass propertyType = readMethod.getReturnType();
block.addStatement(agent("propertyTypes").invoke(
"put",
property,
Stmt.newObject(PropertyType.class, loadLiteral(propertyType.asBoxed()),
DataBindingUtil.isBindableType(propertyType),
propertyType.isAssignableTo(List.class))
)
);
}
}
block.addStatement(agent("propertyTypes").invoke(
"put",
"this",
Stmt.newObject(PropertyType.class, loadLiteral(bindable.asBoxed()),
true,
bindable.isAssignableTo(List.class))
)
);
return (block.isEmpty()) ? EmptyStatement.INSTANCE : block;
}
/**
* Generates method body for recursively unwrapping a {@link BindableProxy}.
*/
private Statement generateDeepUnwrapMethodBody(final String methodName) {
final String cloneVar = "clone";
final BlockStatement block = new BlockStatement();
block.addStatement(Stmt.declareFinalVariable(cloneVar, bindable, Stmt.newObject(bindable)));
for (final String property : bindable.getBeanDescriptor().getProperties()) {
final MetaMethod readMethod = bindable.getBeanDescriptor().getReadMethodForProperty(property);
final MetaMethod writeMethod = bindable.getBeanDescriptor().getWriteMethodForProperty(property);
if (readMethod != null && writeMethod != null) {
final MetaClass type = readMethod.getReturnType();
if (!DataBindingUtil.isBindableType(type)) {
// If we find a collection we copy its elements and unwrap them if necessary
// TODO support map types
if (type.isAssignableTo(Collection.class)) {
final String colVarName = property + "Clone";
final String elemVarName = property + "Elem";
final BlockBuilder<ElseBlockBuilder> colBlock = If.isNotNull(Stmt.nestedCall(target().invoke(readMethod)));
if ((type.isInterface() || type.isAbstract()) &&
(type.isAssignableTo(List.class) || type.isAssignableTo(Set.class))) {
final MetaClass clazz = (type.isAssignableTo(Set.class))
? MetaClassFactory.get(HashSet.class) : MetaClassFactory.get(ArrayList.class);
colBlock.append(Stmt.declareFinalVariable(colVarName, type.getErased(), Stmt.newObject(clazz)));
}
else {
if (!type.isInterface() && !type.isAbstract()) {
colBlock.append(Stmt.declareFinalVariable(colVarName, type.getErased(), Stmt.newObject(type.getErased())));
}
else {
logger.log(TreeLogger.WARN, "Bean validation on collection " + property + " in class " + bindable +
" won't work. Change to either List or Set or use a concrete type instead.");
continue;
}
}
// Check if the collection element is proxied and unwrap if necessary
colBlock.append(
Stmt.nestedCall(target().invoke(readMethod)).foreach(elemVarName, Object.class)
.append (
If.instanceOf(Refs.get(elemVarName), BindableProxy.class)
.append (Stmt.loadVariable(colVarName)
.invoke("add", Stmt.castTo(BindableProxy.class, Stmt.loadVariable(elemVarName)).invoke(methodName))
)
.finish()
.else_()
.append(Stmt.loadVariable(colVarName).invoke("add", Refs.get(elemVarName)))
.finish()
)
.finish());
colBlock.append(Stmt.loadVariable(cloneVar).invoke(writeMethod, Refs.get(colVarName)));
block.addStatement(colBlock.finish());
}
else {
block.addStatement(Stmt.loadVariable(cloneVar).invoke(writeMethod,target().invoke(readMethod)));
}
}
// Found a bindable property: Generate code to unwrap for the case the instance is proxied
else {
final Statement field = target().invoke(readMethod);
block.addStatement (
If.instanceOf(field, BindableProxy.class)
.append(Stmt.loadVariable(cloneVar).invoke(writeMethod,
Cast.to (
readMethod.getReturnType(),
Stmt.castTo(BindableProxy.class, Stmt.loadVariable("this").invoke(readMethod)).invoke(methodName)
)
)
)
.finish()
.elseif_(Stmt.invokeStatic(BindableProxyFactory.class, "isBindableType", target().invoke(readMethod)))
.append(Stmt.loadVariable(cloneVar).invoke(writeMethod,
Cast.to (
readMethod.getReturnType(),
Stmt.castTo(BindableProxy.class, Stmt.invokeStatic(BindableProxyFactory.class,
"getBindableProxy", target().invoke(readMethod))).invoke(methodName)
)
)
)
.finish()
.else_()
.append(Stmt.loadVariable(cloneVar).invoke(writeMethod, target().invoke(readMethod)))
.finish()
);
}
}
}
block.addStatement(Stmt.loadVariable(cloneVar).returnValue());
return block;
}
private String inferSafeAgentFieldName() {
String fieldName = "agent";
while (bindable.getInheritedField(fieldName) != null) {
fieldName = "_" + fieldName;
}
return fieldName;
}
private String ensureSafeLocalVariableName(String name, final MetaMethod method) {
final MetaParameter[] params = method.getParameters();
if (params != null) {
for (final MetaParameter param : params) {
if (name.equals(param.getName())) {
name = "_" + name;
break;
}
}
}
return name;
}
private ContextualStatementBuilder agent(final String field) {
return agent().loadField(field);
}
private ContextualStatementBuilder agent() {
return Stmt.loadClassMember(agentField);
}
private ContextualStatementBuilder target() {
return Stmt.nestedCall(new Statement() {
@Override
public String generate(final Context context) {
return agent().loadField("target").generate(context);
}
@Override
public MetaClass getType() {
return bindable;
}
});
}
private MetaClass getTypeOrFirstUpperBound(MetaType clazz, final MetaMethod method) {
if (clazz instanceof MetaTypeVariable) {
final MetaType[] bounds = ((MetaTypeVariable) clazz).getBounds();
if (bounds.length == 1 && bounds[0] instanceof MetaClass) {
clazz = ((MetaTypeVariable) clazz).getBounds()[0];
}
else {
// TODO add full support for generics in errai codegen
logger.log(TreeLogger.WARN, "Ignoring method: " + method + " in class " + bindable + ". Methods using " +
"multiple type parameters or type parameters with multiple bounds are currently not supported in " +
"@Bindable types! Invoking this method on a bound model will have unpredictable results.");
return null;
}
}
else if (clazz instanceof MetaParameterizedType) {
clazz = ((MetaParameterizedType) clazz).getRawType();
}
if (clazz instanceof MetaClass) {
return (MetaClass) clazz;
}
logger.log(TreeLogger.WARN, "Ignoring method: " + method + " in class " + bindable + ". Method cannot be proxied!");
return null;
}
}