Home | History | Annotate | Download | only in expr
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.databinding.tool.expr;
     18 
     19 import android.databinding.tool.Binding;
     20 import android.databinding.tool.BindingTarget;
     21 import android.databinding.tool.InverseBinding;
     22 import android.databinding.tool.ext.ExtKt;
     23 import android.databinding.tool.processing.ErrorMessages;
     24 import android.databinding.tool.processing.Scope;
     25 import android.databinding.tool.reflection.Callable;
     26 import android.databinding.tool.reflection.Callable.Type;
     27 import android.databinding.tool.reflection.ModelAnalyzer;
     28 import android.databinding.tool.reflection.ModelClass;
     29 import android.databinding.tool.store.SetterStore;
     30 import android.databinding.tool.store.SetterStore.BindingGetterCall;
     31 import android.databinding.tool.util.BrNameUtil;
     32 import android.databinding.tool.util.L;
     33 import android.databinding.tool.util.Preconditions;
     34 import android.databinding.tool.writer.KCode;
     35 
     36 import com.google.common.collect.Lists;
     37 
     38 import java.util.List;
     39 
     40 public class FieldAccessExpr extends MethodBaseExpr {
     41     // notification name for the field. Important when we map this to a method w/ different name
     42     String mBrName;
     43     Callable mGetter;
     44     boolean mIsListener;
     45     boolean mIsViewAttributeAccess;
     46 
     47     FieldAccessExpr(Expr parent, String name) {
     48         super(parent, name);
     49         mName = name;
     50     }
     51 
     52     public Callable getGetter() {
     53         if (mGetter == null) {
     54             getResolvedType();
     55         }
     56         return mGetter;
     57     }
     58 
     59     @Override
     60     public String getInvertibleError() {
     61         if (getGetter() == null) {
     62             return "Listeners do not support two-way binding";
     63         }
     64         if (mGetter.setterName == null) {
     65             return "Two-way binding cannot resolve a setter for " + getResolvedType().toJavaCode() +
     66                     " property '" + mName + "'";
     67         }
     68         if (!mGetter.isDynamic()) {
     69             return "Cannot change a final field in " + getResolvedType().toJavaCode() +
     70                     " property " + mName;
     71         }
     72         return null;
     73     }
     74 
     75     public int getMinApi() {
     76         return mGetter == null ? 0 : mGetter.getMinApi();
     77     }
     78 
     79     @Override
     80     public boolean isDynamic() {
     81         if (mGetter == null) {
     82             getResolvedType();
     83         }
     84         if (mGetter == null || mGetter.type == Type.METHOD) {
     85             return true;
     86         }
     87         // if it is static final, gone
     88         if (getTarget().isDynamic()) {
     89             // if owner is dynamic, then we can be dynamic unless we are static final
     90             return !mGetter.isStatic() || mGetter.isDynamic();
     91         }
     92 
     93         if (mIsViewAttributeAccess) {
     94             return true; // must be able to invalidate this
     95         }
     96 
     97         // if owner is NOT dynamic, we can be dynamic if an only if getter is dynamic
     98         return mGetter.isDynamic();
     99     }
    100 
    101     public boolean hasBindableAnnotations() {
    102         return mGetter != null && mGetter.canBeInvalidated();
    103     }
    104 
    105     @Override
    106     public Expr resolveListeners(ModelClass listener, Expr parent) {
    107         final ModelClass targetType = getTarget().getResolvedType();
    108         if (getGetter() == null && (listener == null || !mIsListener)) {
    109             L.e("Could not resolve %s.%s as an accessor or listener on the attribute.",
    110                     targetType.getCanonicalName(), mName);
    111             return this;
    112         }
    113         try {
    114             Expr listenerExpr = resolveListenersAsMethodReference(listener, parent);
    115             L.w("Method references using '.' is deprecated. Instead of '%s', use '%s::%s'",
    116                     toString(), getTarget(), getName());
    117             return listenerExpr;
    118         } catch (IllegalStateException e) {
    119             if (getGetter() == null) {
    120                 L.e("%s", e.getMessage());
    121             }
    122             return this;
    123         }
    124     }
    125 
    126     @Override
    127     protected String computeUniqueKey() {
    128         return join(mName, ".", getTarget().getUniqueKey());
    129     }
    130 
    131     public String getBrName() {
    132         if (mIsListener) {
    133             return null;
    134         }
    135         try {
    136             Scope.enter(this);
    137             Preconditions.checkNotNull(mGetter, "cannot get br name before resolving the getter");
    138             return mBrName;
    139         } finally {
    140             Scope.exit();
    141         }
    142     }
    143 
    144     @Override
    145     protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) {
    146         if (mIsListener) {
    147             return modelAnalyzer.findClass(Object.class);
    148         }
    149         if (mGetter == null) {
    150             Expr target = getTarget();
    151             target.getResolvedType();
    152             boolean isStatic = target instanceof StaticIdentifierExpr;
    153             ModelClass resolvedType = target.getResolvedType();
    154             L.d("resolving %s. Resolved class type: %s", this, resolvedType);
    155 
    156             mGetter = resolvedType.findGetterOrField(mName, isStatic);
    157 
    158             if (mGetter == null) {
    159                 mIsListener = !resolvedType.findMethods(mName, isStatic).isEmpty();
    160                 if (!mIsListener) {
    161                     L.e("Could not find accessor %s.%s", resolvedType.getCanonicalName(), mName);
    162                 }
    163                 return modelAnalyzer.findClass(Object.class);
    164             }
    165 
    166             if (mGetter.isStatic() && !isStatic) {
    167                 // found a static method on an instance. register a new one
    168                 replaceStaticIdentifier(resolvedType);
    169                 target = getTarget();
    170             }
    171 
    172             if (mGetter.resolvedType.isObservableField()) {
    173                 // Make this the ".get()" and add an extra field access for the observable field
    174                 target.getParents().remove(this);
    175                 getChildren().remove(target);
    176 
    177                 FieldAccessExpr observableField = getModel().observableField(target, mName);
    178                 getChildren().add(observableField);
    179                 observableField.getParents().add(this);
    180                 mGetter = mGetter.resolvedType.findGetterOrField("", false);
    181                 mName = "";
    182                 mBrName = ExtKt.br(mName);
    183             } else if (hasBindableAnnotations()) {
    184                 mBrName = ExtKt.br(BrNameUtil.brKey(mGetter));
    185             }
    186         }
    187         return mGetter.resolvedType;
    188     }
    189 
    190     protected void replaceStaticIdentifier(ModelClass staticIdentifierType) {
    191         getTarget().getParents().remove(this);
    192         getChildren().remove(getTarget());
    193         StaticIdentifierExpr staticId = getModel().staticIdentifierFor(staticIdentifierType);
    194         getChildren().add(staticId);
    195         staticId.getParents().add(this);
    196     }
    197 
    198     @Override
    199     public Expr resolveTwoWayExpressions(Expr parent) {
    200         final Expr child = getTarget();
    201         if (!(child instanceof ViewFieldExpr)) {
    202             return this;
    203         }
    204         final ViewFieldExpr expr = (ViewFieldExpr) child;
    205         final BindingTarget bindingTarget = expr.getBindingTarget();
    206 
    207         // This is a binding to a View's attribute, so look for matching attribute
    208         // on that View's BindingTarget. If there is an expression, we simply replace
    209         // the binding with that binding expression.
    210         for (Binding binding : bindingTarget.getBindings()) {
    211             if (attributeMatchesName(binding.getName(), mName)) {
    212                 final Expr replacement = binding.getExpr();
    213                 replaceExpression(parent, replacement);
    214                 return replacement;
    215             }
    216         }
    217 
    218         // There was no binding expression to bind to. This should be a two-way binding.
    219         // This is a synthesized two-way binding because we must capture the events from
    220         // the View and change the value when the target View's attribute changes.
    221         final SetterStore setterStore = SetterStore.get(ModelAnalyzer.getInstance());
    222         final ModelClass targetClass = expr.getResolvedType();
    223         BindingGetterCall getter = setterStore.getGetterCall(mName, targetClass, null, null);
    224         if (getter == null) {
    225             getter = setterStore.getGetterCall("android:" + mName, targetClass, null, null);
    226             if (getter == null) {
    227                 L.e("Could not resolve the two-way binding attribute '%s' on type '%s'",
    228                         mName, targetClass);
    229             }
    230         }
    231         InverseBinding inverseBinding = null;
    232         for (Binding binding : bindingTarget.getBindings()) {
    233             final Expr testExpr = binding.getExpr();
    234             if (testExpr instanceof TwoWayListenerExpr &&
    235                     getter.getEventAttribute().equals(binding.getName())) {
    236                 inverseBinding = ((TwoWayListenerExpr) testExpr).mInverseBinding;
    237                 break;
    238             }
    239         }
    240         if (inverseBinding == null) {
    241             inverseBinding = bindingTarget.addInverseBinding(mName, getter);
    242         }
    243         inverseBinding.addChainedExpression(this);
    244         mIsViewAttributeAccess = true;
    245         enableDirectInvalidation();
    246         return this;
    247     }
    248 
    249     private static boolean attributeMatchesName(String attribute, String field) {
    250         int colonIndex = attribute.indexOf(':');
    251         return attribute.substring(colonIndex + 1).equals(field);
    252     }
    253 
    254     private void replaceExpression(Expr parent, Expr replacement) {
    255         if (parent != null) {
    256             List<Expr> children = parent.getChildren();
    257             int index;
    258             while ((index = children.indexOf(this)) >= 0) {
    259                 children.set(index, replacement);
    260                 replacement.getParents().add(parent);
    261             }
    262             while (getParents().remove(parent)) {
    263                 // just remove all copies of parent.
    264             }
    265         }
    266         if (getParents().isEmpty()) {
    267             getModel().removeExpr(this);
    268         }
    269     }
    270 
    271     @Override
    272     protected String asPackage() {
    273         String parentPackage = getTarget().asPackage();
    274         return parentPackage == null ? null : parentPackage + "." + mName;
    275     }
    276 
    277     @Override
    278     protected KCode generateCode() {
    279         // once we can deprecate using Field.access for callbacks, we can get rid of this since
    280         // it will be detected when resolve type is run.
    281         Preconditions.checkNotNull(getGetter(), ErrorMessages.CANNOT_RESOLVE_TYPE, this);
    282         KCode code = new KCode()
    283                 .app("", getTarget().toCode()).app(".");
    284         if (getGetter().type == Callable.Type.FIELD) {
    285             return code.app(getGetter().name);
    286         } else {
    287             return code.app(getGetter().name).app("()");
    288         }
    289     }
    290 
    291     @Override
    292     public Expr generateInverse(ExprModel model, Expr value, String bindingClassName) {
    293         Expr castExpr = model.castExpr(getResolvedType().toJavaCode(), value);
    294         Expr target = getTarget().cloneToModel(model);
    295         Expr result;
    296         if (getGetter().type == Callable.Type.FIELD) {
    297             result = model.assignment(target, mName, castExpr);
    298         } else {
    299             result = model.methodCall(target, mGetter.setterName, Lists.newArrayList(castExpr));
    300         }
    301         return result;
    302     }
    303 
    304     @Override
    305     public Expr cloneToModel(ExprModel model) {
    306         final Expr clonedTarget = getTarget().cloneToModel(model);
    307         return model.field(clonedTarget, mName);
    308     }
    309 
    310     @Override
    311     public String toString() {
    312         String name = mName.isEmpty() ? "get()" : mName;
    313         return getTarget().toString() + '.' + name;
    314     }
    315 }
    316