/*
* Copyright 2011 JBoss, by Red Hat, Inc
*
* 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.meta.MetaClassFactory.parameterizedAs;
import static org.jboss.errai.codegen.meta.MetaClassFactory.typeParametersOf;
import org.jboss.errai.codegen.BlockStatement;
import org.jboss.errai.codegen.Cast;
import org.jboss.errai.codegen.Context;
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.impl.ClassBuilder;
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.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.BindableProxyState;
import org.jboss.errai.databinding.client.HasProperties;
import org.jboss.errai.databinding.client.HasPropertyChangeHandlers;
import org.jboss.errai.databinding.client.NonExistingPropertyException;
import org.jboss.errai.databinding.client.api.Bindable;
import org.jboss.errai.databinding.client.api.Convert;
import org.jboss.errai.databinding.client.api.Converter;
import org.jboss.errai.databinding.client.api.DataBinder;
import org.jboss.errai.databinding.client.api.InitialState;
import org.jboss.errai.databinding.client.api.PropertyChangeEvent;
import org.jboss.errai.databinding.client.api.PropertyChangeHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Widget;
/**
* Generates a proxy for a {@link Bindable} type.
* <p>
* The proxy will:
* <ul>
* <li>Carry out an initial state sync between the bound widgets and the target model, if specified (see
* {@link DataBinder#DataBinder(Object, InitialState)})</li>
* <li>Update the bound widget when a setter method is invoked on the model (works for widgets that either implement
* {@link HasValue} or {@link HasText})</li>
* <li>Update the target model's state in response to value change events (only works for widgets that implement
* {@link HasValue})</li>
* <ul>
*
* @author Christian Sadilek <csadilek@redhat.com>
*/
public class BindableProxyGenerator {
private final MetaClass bindable;
public BindableProxyGenerator(MetaClass bindable) {
this.bindable = bindable;
}
public ClassStructureBuilder<?> generate() {
ClassStructureBuilder<?> classBuilder = ClassBuilder.define(bindable.getName() + "Proxy", bindable)
.packageScope()
.implementsInterface(BindableProxy.class)
.implementsInterface(HasProperties.class)
.implementsInterface(HasPropertyChangeHandlers.class)
.body();
classBuilder
.privateField("state", parameterizedAs(BindableProxyState.class, typeParametersOf(bindable)))
.finish()
.publicConstructor(Parameter.of(InitialState.class, "initialState"))
.callThis(Stmt.newObject(bindable), Variable.get("initialState"))
.finish()
.publicConstructor(Parameter.of(bindable, "target"), Parameter.of(InitialState.class, "initialState"))
.append(Stmt.loadVariable("state").assignValue(
Stmt.newObject(parameterizedAs(BindableProxyState.class, typeParametersOf(bindable)),
Variable.get("target"), Variable.get("initialState"))))
.append(generatePropertiesMap())
.finish()
.publicMethod(BindableProxyState.class, "getState")
.append(Stmt.loadClassMember("state").returnValue())
.finish()
.publicMethod(void.class, "updateWidgets")
.append(Stmt.loadVariable("this").invoke("syncState", Stmt.loadStatic(InitialState.class, "FROM_MODEL")))
.finish()
.publicMethod(bindable, "unwrap")
.append(target().returnValue())
.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();
generateStateSyncMethods(classBuilder);
generateProxyBindMethod(classBuilder);
generateProxyUnbindMethods(classBuilder);
generateProxyAccessorMethods(classBuilder);
generateHasPropertyChangeHandlersMethods(classBuilder);
return classBuilder;
}
private void generateProxyBindMethod(ClassStructureBuilder<?> classBuilder) {
BlockBuilder<?> bindMethodBuilder =
classBuilder.publicMethod(void.class, "bind", Parameter.of(Widget.class, "widget", true),
Parameter.of(String.class, "property", true), Parameter.of(Converter.class, "converter", true))
.append(
// This call ensures an exception is thrown for bindings to non existing properties.
// Reusing this method for this purpose helps to keep the generated code size smaller.
Stmt.loadVariable("this").invoke("get", Variable.get("property")))
.append(
Stmt.loadVariable("this").invoke("unbind", Variable.get("property")))
.append(
If.cond(field("bindings").invoke("containsValue", Variable.get("widget")))
.append(Stmt.throw_(RuntimeException.class, "Widget already bound to a different property!"))
.finish())
.append(
field("bindings").invoke("put", Variable.get("property"), Variable.get("widget")))
.append(
field("converters").invoke("put", Variable.get("property"), Variable.get("converter")))
.append(
If.instanceOf(Variable.get("widget"), HasValue.class)
.append(field("handlerRegistrations").invoke(
"put",
Variable.get("property"),
Stmt.castTo(HasValue.class, Stmt.loadVariable("widget")).invoke("addValueChangeHandler",
Stmt.newObject(ValueChangeHandler.class).extend()
.publicOverridesMethod("onValueChange",
Parameter.of(ValueChangeEvent.class, "event"))
.append(
Stmt.loadStatic(classBuilder.getClassDefinition(), "this").invoke("set",
Variable.get("property"),
Stmt.nestedCall(Stmt.loadVariable("event").invoke("getValue"))))
.finish()
.finish()
)
)
).finish()
)
.append(
Stmt.loadVariable("this").invoke("syncState", Variable.get("widget"), Variable.get("property"),
field("initialState")));
bindMethodBuilder.finish();
}
private void generateStateSyncMethods(ClassStructureBuilder<?> classBuilder) {
generateStateSyncForBindings(classBuilder);
classBuilder
.privateMethod(void.class, "syncState", Parameter.of(Widget.class, "widget", true),
Parameter.of(String.class, "property", true), Parameter.of(InitialState.class, "initialState", true))
.append(
If.isNotNull(Variable.get("initialState"))
.append(Stmt.declareVariable("value", Object.class, null))
.append(
If.instanceOf(Variable.get("widget"), HasValue.class)
.append(
Stmt.declareVariable("hasValue", HasValue.class,
Stmt.castTo(HasValue.class, Stmt.loadVariable("widget"))))
.append(
Stmt.loadVariable("value").assignValue(
Stmt.loadVariable("initialState").invoke("getInitialValue",
Stmt.loadVariable("this").invoke("get", Variable.get("property")),
Stmt.loadVariable("hasValue").invoke("getValue"))))
.append(
If.idEquals(Variable.get("initialState"), Stmt.loadStatic(InitialState.class, "FROM_MODEL"))
.append(
Stmt.loadVariable("hasValue").invoke(
"setValue",
Stmt.invokeStatic(Convert.class, "toWidgetValue",
Variable.get("widget"),
field("propertyTypes").invoke("get", Variable.get("property")),
Stmt.loadVariable("value"),
field("converters").invoke("get", Variable.get("property")))))
.finish())
.finish()
.elseif_(
Bool.instanceOf(Variable.get("widget"), HasText.class))
.append(
Stmt.declareVariable("hasText", HasText.class,
Stmt.castTo(HasText.class, Stmt.loadVariable("widget"))))
.append(
Stmt.loadVariable("value").assignValue(
Stmt.loadVariable("initialState").invoke("getInitialValue",
Stmt.loadVariable("this").invoke("get", Variable.get("property")),
Stmt.loadVariable("hasText").invoke("getText"))))
.append(
If.idEquals(Variable.get("initialState"), Stmt.loadStatic(InitialState.class, "FROM_MODEL"))
.append(
Stmt.loadVariable("hasText").invoke(
"setText",
Stmt.castTo(String.class, Stmt.invokeStatic(Convert.class, "toWidgetValue",
String.class,
field("propertyTypes").invoke("get", Variable.get("property")),
Stmt.loadVariable("value"),
field("converters").invoke("get", Variable.get("property"))))))
.finish())
.finish()
)
.append(If.idEquals(Variable.get("initialState"), Stmt.loadStatic(InitialState.class, "FROM_UI"))
.append(
Stmt.loadVariable("this").invoke("set", Variable.get("property"), Variable.get("value")))
.finish())
.finish())
.finish();
}
private void generateStateSyncForBindings(ClassStructureBuilder<?> classBuilder) {
classBuilder.privateMethod(void.class, "syncState", Parameter.of(InitialState.class, "initialState", true))
.append(
field("bindings").invoke("keySet").foreach("property")
.append(
Stmt.loadVariable("this")
.invoke("syncState",
field("bindings").invoke("get", Variable.get("property")),
Stmt.castTo(String.class, Stmt.loadVariable("property")),
Variable.get("initialState")))
.finish())
.finish();
}
private void generateProxyUnbindMethods(ClassStructureBuilder<?> classBuilder) {
classBuilder.publicMethod(void.class, "unbind", Parameter.of(String.class, "property"))
.append(
Stmt.loadClassMember("state").invoke("unbind", Variable.get("property")))
.finish();
classBuilder.publicMethod(void.class, "unbind")
.append(
Stmt.loadClassMember("state").invoke("unbind"))
.finish();
}
private void generateProxyAccessorMethods(ClassStructureBuilder<?> classBuilder) {
BlockBuilder<?> getMethod = classBuilder.publicMethod(Object.class, "get",
Parameter.of(String.class, "property"));
BlockBuilder<?> setMethod = classBuilder.publicMethod(void.class, "set",
Parameter.of(String.class, "property"),
Parameter.of(Object.class, "value"));
for (String property : bindable.getBeanDescriptor().getProperties()) {
generateGetter(classBuilder, property, getMethod);
generateSetter(classBuilder, property, setMethod);
}
Statement nonExistingPropertyException = Stmt.throw_(NonExistingPropertyException.class, Variable.get("property"));
getMethod.append(nonExistingPropertyException).finish();
setMethod.append(nonExistingPropertyException).finish();
}
private void generateGetter(ClassStructureBuilder<?> classBuilder, String property,
BlockBuilder<?> getMethod) {
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();
}
}
private void generateSetter(ClassStructureBuilder<?> classBuilder, String property, BlockBuilder<?> setMethod) {
MetaMethod getterMethod = bindable.getBeanDescriptor().getReadMethodForProperty(property);
MetaMethod setterMethod = bindable.getBeanDescriptor().getWriteMethodForProperty(property);
if (setterMethod != null && !setterMethod.isFinal()) {
setMethod.append(
If.cond(Stmt.loadVariable("property").invoke("equals", property))
.append(
Stmt.declareVariable("oldValue", setterMethod.getParameters()[0].getType().asBoxed(),
Stmt.loadVariable("this").invoke(getterMethod)))
.append(
target().invoke(
setterMethod.getName(),
Cast.to(setterMethod.getParameters()[0].getType().asBoxed(),
Stmt.invokeStatic(Convert.class, "toModelValue",
setterMethod.getParameters()[0].getType().asBoxed().asClass(),
field("bindings").invoke("get", Variable.get("property")),
Stmt.loadVariable("value"),
field("converters").invoke("get", Variable.get("property"))))))
.append(
field("propertyChangeHandlerSupport").invoke("notifyHandlers",
Stmt.newObject(PropertyChangeEvent.class, Stmt.loadVariable("this"),
Stmt.loadVariable("property"), Stmt.loadVariable("oldValue"),
Stmt.loadVariable("value"))))
.append(
Stmt.returnVoid())
.finish()
);
MetaClass paramType = setterMethod.getParameters()[0].getType();
Statement callSetterOnTarget = target().invoke(setterMethod.getName(),
Cast.to(paramType, Stmt.loadVariable(property)));
// If the set method we are proxying returns a value, capture that value into a local variable
Statement returnValueOfSetter = EmptyStatement.INSTANCE;
if (!setterMethod.getReturnType().equals(MetaClassFactory.get(void.class))) {
callSetterOnTarget =
Stmt.declareFinalVariable("returnValueOfSetter", setterMethod.getReturnType(), callSetterOnTarget);
returnValueOfSetter = Stmt.nestedCall(Refs.get("returnValueOfSetter")).returnValue();
}
classBuilder.publicMethod(setterMethod.getReturnType(), setterMethod.getName(),
Parameter.of(paramType, property))
.append(
Stmt.declareVariable("oldValue", Object.class, target().invoke(getterMethod.getName())))
.append(callSetterOnTarget)
.append(Stmt.declareVariable("widget", Widget.class, field("bindings").invoke("get", property)))
.append(
If.instanceOf(Variable.get("widget"), HasValue.class)
.append(
Stmt.castTo(HasValue.class, Stmt.loadVariable("widget")).invoke(
"setValue",
Stmt.invokeStatic(Convert.class, "toWidgetValue",
Variable.get("widget"),
paramType.asBoxed().asClass(),
Stmt.castTo(paramType.asBoxed(), Stmt.loadVariable(property)),
field("converters").invoke("get", property)),
true))
.finish()
.elseif_(
Bool.instanceOf(Variable.get("widget"), HasText.class))
.append(
Stmt.castTo(HasText.class, Stmt.loadVariable("widget"))
.invoke(
"setText",
Stmt.castTo(String.class,
Stmt.invokeStatic(Convert.class, "toWidgetValue", String.class,
paramType.asBoxed().asClass(),
Stmt.castTo(paramType.asBoxed(), Stmt.loadVariable(property)),
field("converters").invoke("get", property)
)
)
)
)
.finish()
)
.append(
Stmt.declareVariable("event", PropertyChangeEvent.class,
Stmt.newObject(PropertyChangeEvent.class, Stmt.loadVariable("this"), property,
Stmt.loadVariable("oldValue"), Stmt.loadVariable(property))
)
)
.append(
field("propertyChangeHandlerSupport").invoke("notifyHandlers", Stmt.loadVariable("event")))
.append(returnValueOfSetter)
.finish();
}
}
private void generateHasPropertyChangeHandlersMethods(ClassStructureBuilder<?> classBuilder) {
classBuilder.publicMethod(void.class, "addPropertyChangeHandler",
Parameter.of(PropertyChangeHandler.class, "handler"))
.append(
field("propertyChangeHandlerSupport").invoke("addPropertyChangeHandler", Variable.get("handler")))
.finish();
classBuilder.publicMethod(void.class, "addPropertyChangeHandler",
Parameter.of(String.class, "name"),
Parameter.of(PropertyChangeHandler.class, "handler"))
.append(
field("propertyChangeHandlerSupport").invoke("addPropertyChangeHandler",
Variable.get("name"), Variable.get("handler")))
.finish();
classBuilder.publicMethod(void.class, "removePropertyChangeHandler",
Parameter.of(PropertyChangeHandler.class, "handler"))
.append(
field("propertyChangeHandlerSupport").invoke("removePropertyChangeHandler", Variable.get("handler")))
.finish();
classBuilder.publicMethod(void.class, "removePropertyChangeHandler",
Parameter.of(String.class, "name"),
Parameter.of(PropertyChangeHandler.class, "handler"))
.append(
field("propertyChangeHandlerSupport").invoke("removePropertyChangeHandler",
Variable.get("name"), Variable.get("handler")))
.finish();
}
private Statement generatePropertiesMap() {
BlockStatement block = new BlockStatement();
for (String property : bindable.getBeanDescriptor().getProperties()) {
MetaMethod readMethod = bindable.getBeanDescriptor().getReadMethodForProperty(property);
if (!readMethod.isFinal()) {
block.addStatement(field("propertyTypes").invoke("put", property,
readMethod.getReturnType().asBoxed().asClass()));
}
}
return block;
}
private ContextualStatementBuilder field(String field) {
return Stmt.loadClassMember("state").loadField(field);
}
private ContextualStatementBuilder target() {
return Stmt.nestedCall(new Statement() {
@Override
public String generate(Context context) {
return Stmt.loadClassMember("state").loadField("target").generate(context);
}
@Override
public MetaClass getType() {
return bindable;
}
});
}
}