/*
* 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.databinding.rebind;
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.throw_;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Inject;
import org.jboss.errai.codegen.Statement;
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.util.If;
import org.jboss.errai.codegen.util.PrivateAccessUtil;
import org.jboss.errai.codegen.util.Refs;
import org.jboss.errai.codegen.util.Stmt;
import org.jboss.errai.common.client.api.IsElement;
import org.jboss.errai.common.client.ui.ElementWrapperWidget;
import org.jboss.errai.common.client.ui.HasValue;
import org.jboss.errai.databinding.client.BoundUtil;
import org.jboss.errai.databinding.client.api.Convert;
import org.jboss.errai.databinding.client.api.DataBinder;
import org.jboss.errai.databinding.client.api.handler.list.BindableListChangeHandler;
import org.jboss.errai.ioc.client.api.CodeDecorator;
import org.jboss.errai.ioc.client.container.InitializationCallback;
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.Decorable.DecorableType;
import org.jboss.errai.ioc.rebind.ioc.injector.api.FactoryController;
import org.jboss.errai.ui.shared.api.annotations.Bound;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.TakesValue;
import com.google.gwt.user.client.ui.Widget;
import jsinterop.annotations.JsType;
/**
* Generates an {@link InitializationCallback} that contains automatic binding logic.
*
* @author Christian Sadilek <csadilek@redhat.com>
*/
// Must run after TemplatedCodeDecorator
@CodeDecorator(order=2)
public class BoundDecorator extends IOCDecoratorExtension<Bound> {
final Set<MetaClass> processedTypes = Collections.newSetFromMap(new ConcurrentHashMap<MetaClass, Boolean>());
public BoundDecorator(Class<Bound> decoratesWith) {
super(decoratesWith);
}
@Override
public void generateDecorator(final Decorable decorable, final FactoryController controller) {
final MetaClass targetClass = decorable.getEnclosingInjectable().getInjectedType();
final List<Statement> statements = new ArrayList<Statement>();
final boolean hasRunForType = processedTypes.contains(targetClass);
final DataBindingUtil.DataBinderRef binderLookup = DataBindingUtil.lookupDataBinderRef(decorable, controller);
if (binderLookup != null) {
// Generate a reference to the bean's @AutoBound data binder
if (!hasRunForType) {
statements.add(declareVariable("binder", DataBinder.class, binderLookup.getValueAccessor()));
statements.add(If.isNull(Refs.get("binder")).append(
throw_(RuntimeException.class, "@AutoBound data binder for class "
+ targetClass
+ " has not been initialized. Either initialize or add @Inject!")).finish());
statements.add(controller.setReferenceStmt(DataBindingUtil.BINDER_VAR_NAME, loadVariable("binder")));
}
// Check if the bound property exists in data model type
final Bound bound = (Bound) decorable.getAnnotation();
final boolean propertyIsEmpty = bound.property().equals("");
String property = propertyIsEmpty ? decorable.getName() : bound.property();
if (!DataBindingValidator.isValidPropertyChain(binderLookup.getDataModelType(), property)) {
if (propertyIsEmpty && binderLookup.getDataModelType().equals(getValueType(decorable.getType()))) {
property = "this";
}
else {
throw new GenerationException("Invalid binding of field " + decorable.getName()
+ " in class " + targetClass + "! Property " + property
+ " not resolvable from class " + binderLookup.getDataModelType()
+ "! Hint: Is " + binderLookup.getDataModelType() + " marked as @Bindable? When binding to a "
+ "property chain, all properties but the last in a chain must be of a @Bindable type!");
}
}
Statement component = decorable.getAccessStatement();
controller.ensureMemberExposed(decorable.get());
// Ensure the @Bound field or method provides a widget or DOM element
MetaClass componentType = decorable.getType();
if (componentType.isAssignableTo(Widget.class)) {
// Ensure @Bound widget field is initialized
if (!decorable.get().isAnnotationPresent(Inject.class) && decorable.decorableType().equals(DecorableType.FIELD) && componentType.isDefaultInstantiable()) {
Statement widgetInit = Stmt.loadVariable("this").invoke(
PrivateAccessUtil.getPrivateFieldAccessorName(decorable.getAsField()),
Refs.get("instance"),
ObjectBuilder.newInstanceOf(componentType));
statements.add(If.isNull(component).append(widgetInit).finish());
}
}
else if (componentType.isAnnotationPresent(JsType.class)) {
if (componentType.isAssignableTo(HasValue.class)) {
final MetaClass valueType = componentType.getMethod("getValue", new Class[0]).getReturnType();
component = Stmt.invokeStatic(ElementWrapperWidget.class, "getWidget",
Stmt.invokeStatic(BoundUtil.class, "asElement", component), Stmt.loadLiteral(valueType));
}
else {
component = Stmt.invokeStatic(ElementWrapperWidget.class, "getWidget", Stmt.invokeStatic(BoundUtil.class, "asElement", component));
}
}
else if (!(componentType.isAssignableTo(TakesValue.class)
|| componentType.isAssignableTo(BindableListChangeHandler.class)
|| componentType.isAssignableTo(Element.class)
|| componentType.isAnnotationPresent(JsType.class)
|| componentType.isAssignableTo(IsElement.class))) {
throw new GenerationException("@Bound field or method " + decorable.getName()
+ " in class " + targetClass
+ " must be assignable to Widget, TakesValue, or a DOM element type but provides: "
+ componentType.getFullyQualifiedName());
}
// Generate the binding
Statement conv = coverterStatement(bound, decorable.getType(),
DataBindingValidator.getPropertyType(binderLookup.getDataModelType(), property));
Statement onKeyUp = Stmt.load(bound.onKeyUp());
statements.add(Stmt.loadVariable("binder").invoke("bind", component, property, conv, loadLiteral(null), onKeyUp));
}
else {
throw new GenerationException("No @Model or @AutoBound data binder found for @Bound field or method "
+ decorable.getName() + " in class " + targetClass);
}
processedTypes.add(targetClass);
controller.setAttribute(DataBindingUtil.BINDER_MODEL_TYPE_VALUE, binderLookup.getDataModelType());
controller.addInitializationStatements(statements);
if (!hasRunForType) {
controller.addDestructionStatements(Collections.<Statement> singletonList(
nestedCall(controller.getReferenceStmt(DataBindingUtil.BINDER_VAR_NAME, DataBinder.class)).invoke("unbind")));
}
}
private MetaClass getValueType(final MetaClass type) {
if (type.isAssignableTo(TakesValue.class)) {
return type.getMethod("getValue", new Class[0]).getReturnType();
}
else {
return null;
}
}
private Statement coverterStatement(final Bound bound, final MetaClass boundType, final MetaClass propertyType) {
if (bound.converter().equals(Bound.NO_CONVERTER.class)) {
final Optional<MetaClass> valueType;
if (boundType.isAssignableTo(TakesValue.class)) {
valueType = Optional.ofNullable(boundType.getMethod("getValue", new Class[0]).getReturnType());
}
else if (boundType.isAssignableTo(BindableListChangeHandler.class)) {
valueType = Optional.ofNullable(MetaClassFactory.get(List.class));
}
else {
valueType = Optional.empty();
}
return valueType
.map(type -> invokeStatic(Convert.class, "getConverter", loadLiteral(propertyType), loadLiteral(type)))
.orElse(loadLiteral(null));
}
else {
return Stmt.newObject(bound.converter());
}
}
}