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
17package android.databinding.tool.expr;
18
19import android.databinding.tool.Binding;
20import android.databinding.tool.BindingTarget;
21import android.databinding.tool.InverseBinding;
22import android.databinding.tool.ext.ExtKt;
23import android.databinding.tool.processing.ErrorMessages;
24import android.databinding.tool.processing.Scope;
25import android.databinding.tool.reflection.Callable;
26import android.databinding.tool.reflection.Callable.Type;
27import android.databinding.tool.reflection.ModelAnalyzer;
28import android.databinding.tool.reflection.ModelClass;
29import android.databinding.tool.store.SetterStore;
30import android.databinding.tool.store.SetterStore.BindingGetterCall;
31import android.databinding.tool.util.BrNameUtil;
32import android.databinding.tool.util.L;
33import android.databinding.tool.util.Preconditions;
34import android.databinding.tool.writer.KCode;
35
36import com.google.common.collect.Lists;
37
38import java.util.List;
39
40public 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