/* * 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()); } } }