FieldAccessExpr.java revision d3f2b9229472c9dae9bf4ae8b3e2d653b5653b01
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.ext.ExtKt;
20import android.databinding.tool.Binding;
21import android.databinding.tool.BindingTarget;
22import android.databinding.tool.InverseBinding;
23import android.databinding.tool.processing.Scope;
24import android.databinding.tool.reflection.Callable;
25import android.databinding.tool.reflection.Callable.Type;
26import android.databinding.tool.reflection.ModelAnalyzer;
27import android.databinding.tool.reflection.ModelClass;
28import android.databinding.tool.reflection.ModelMethod;
29import android.databinding.tool.util.BrNameUtil;
30import android.databinding.tool.store.SetterStore;
31import android.databinding.tool.store.SetterStore.BindingGetterCall;
32import android.databinding.tool.util.L;
33import android.databinding.tool.util.Preconditions;
34import android.databinding.tool.writer.KCode;
35
36import java.util.List;
37
38public class FieldAccessExpr extends Expr {
39    String mName;
40    // notification name for the field. Important when we map this to a method w/ different name
41    String mBrName;
42    Callable mGetter;
43    final boolean mIsObservableField;
44    boolean mIsListener;
45    boolean mIsViewAttributeAccess;
46
47    FieldAccessExpr(Expr parent, String name) {
48        super(parent);
49        mName = name;
50        mIsObservableField = false;
51    }
52
53    FieldAccessExpr(Expr parent, String name, boolean isObservableField) {
54        super(parent);
55        mName = name;
56        mIsObservableField = isObservableField;
57    }
58
59    public Expr getChild() {
60        return getChildren().get(0);
61    }
62
63    public Callable getGetter() {
64        if (mGetter == null) {
65            getResolvedType();
66        }
67        return mGetter;
68    }
69
70    @Override
71    public String getInvertibleError() {
72        if (getGetter().setterName == null) {
73            return "Two-way binding cannot resolve a setter for " + getResolvedType().toJavaCode() +
74                    " property '" + mName + "'";
75        }
76        if (!mGetter.isDynamic()) {
77            return "Cannot change a final field in " + getResolvedType().toJavaCode() +
78                    " property " + mName;
79        }
80        return null;
81    }
82
83    public int getMinApi() {
84        return mGetter.getMinApi();
85    }
86
87    @Override
88    public boolean isDynamic() {
89        if (mGetter == null) {
90            getResolvedType();
91        }
92        if (mGetter == null || mGetter.type == Type.METHOD) {
93            return true;
94        }
95        // if it is static final, gone
96        if (getChild().isDynamic()) {
97            // if owner is dynamic, then we can be dynamic unless we are static final
98            return !mGetter.isStatic() || mGetter.isDynamic();
99        }
100
101        if (mIsViewAttributeAccess) {
102            return true; // must be able to invalidate this
103        }
104
105        // if owner is NOT dynamic, we can be dynamic if an only if getter is dynamic
106        return mGetter.isDynamic();
107    }
108
109    public boolean hasBindableAnnotations() {
110        return mGetter.canBeInvalidated();
111    }
112
113    @Override
114    public Expr resolveListeners(ModelClass listener, Expr parent) {
115        if (mName == null || mName.isEmpty()) {
116            return this; // ObservableFields aren't listeners
117        }
118        final ModelClass childType = getChild().getResolvedType();
119        if (getGetter() == null) {
120            if (listener == null || !mIsListener) {
121                L.e("Could not resolve %s.%s as an accessor or listener on the attribute.",
122                        childType.getCanonicalName(), mName);
123                return this;
124            }
125            getChild().getParents().remove(this);
126        } else if (listener == null) {
127            return this; // Not a listener, but we have a getter.
128        }
129        List<ModelMethod> abstractMethods = listener.getAbstractMethods();
130        int numberOfAbstractMethods = abstractMethods == null ? 0 : abstractMethods.size();
131        if (numberOfAbstractMethods != 1) {
132            if (mGetter == null) {
133                L.e("Could not find accessor %s.%s and %s has %d abstract methods, so is" +
134                                " not resolved as a listener",
135                        childType.getCanonicalName(), mName,
136                        listener.getCanonicalName(), numberOfAbstractMethods);
137            }
138            return this;
139        }
140
141        // Look for a signature matching the abstract method
142        final ModelMethod listenerMethod = abstractMethods.get(0);
143        final ModelClass[] listenerParameters = listenerMethod.getParameterTypes();
144        boolean isStatic = getChild() instanceof StaticIdentifierExpr;
145        List<ModelMethod> methods = childType.findMethods(mName, isStatic);
146        if (methods == null) {
147            return this;
148        }
149        for (ModelMethod method : methods) {
150            if (acceptsParameters(method, listenerParameters) &&
151                    method.getReturnType(null).equals(listenerMethod.getReturnType(null))) {
152                resetResolvedType();
153                // replace this with ListenerExpr in parent
154                Expr listenerExpr = getModel().listenerExpr(getChild(), mName, listener,
155                        listenerMethod);
156                if (parent != null) {
157                    int index;
158                    while ((index = parent.getChildren().indexOf(this)) != -1) {
159                        parent.getChildren().set(index, listenerExpr);
160                    }
161                }
162                if (getModel().mBindingExpressions.contains(this)) {
163                    getModel().bindingExpr(listenerExpr);
164                }
165                getParents().remove(parent);
166                if (getParents().isEmpty()) {
167                    getModel().removeExpr(this);
168                }
169                return listenerExpr;
170            }
171        }
172
173        if (mGetter == null) {
174            L.e("Listener class %s with method %s did not match signature of any method %s.%s",
175                    listener.getCanonicalName(), listenerMethod.getName(),
176                    childType.getCanonicalName(), mName);
177        }
178        return this;
179    }
180
181    private boolean acceptsParameters(ModelMethod method, ModelClass[] listenerParameters) {
182        ModelClass[] parameters = method.getParameterTypes();
183        if (parameters.length != listenerParameters.length) {
184            return false;
185        }
186        for (int i = 0; i < parameters.length; i++) {
187            if (!parameters[i].isAssignableFrom(listenerParameters[i])) {
188                return false;
189            }
190        }
191        return true;
192    }
193
194    @Override
195    protected List<Dependency> constructDependencies() {
196        final List<Dependency> dependencies = constructDynamicChildrenDependencies();
197        for (Dependency dependency : dependencies) {
198            if (dependency.getOther() == getChild()) {
199                dependency.setMandatory(true);
200            }
201        }
202        return dependencies;
203    }
204
205    @Override
206    protected String computeUniqueKey() {
207        if (mIsObservableField) {
208            return addTwoWay(join(mName, "..", super.computeUniqueKey()));
209        }
210        return addTwoWay(join(mName, ".", super.computeUniqueKey()));
211    }
212
213    public String getName() {
214        return mName;
215    }
216
217    public String getBrName() {
218        if (mIsListener) {
219            return null;
220        }
221        try {
222            Scope.enter(this);
223            Preconditions.checkNotNull(mGetter, "cannot get br name before resolving the getter");
224            return mBrName;
225        } finally {
226            Scope.exit();
227        }
228    }
229
230    @Override
231    public void updateExpr(ModelAnalyzer modelAnalyzer) {
232        try {
233            Scope.enter(this);
234            resolveType(modelAnalyzer);
235            super.updateExpr(modelAnalyzer);
236        } finally {
237            Scope.exit();
238        }
239    }
240
241    @Override
242    protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) {
243        if (mIsListener) {
244            return modelAnalyzer.findClass(Object.class);
245        }
246        if (mGetter == null) {
247            Expr child = getChild();
248            child.getResolvedType();
249            boolean isStatic = child instanceof StaticIdentifierExpr;
250            ModelClass resolvedType = child.getResolvedType();
251            L.d("resolving %s. Resolved class type: %s", this, resolvedType);
252
253            mGetter = resolvedType.findGetterOrField(mName, isStatic);
254
255            if (mGetter == null) {
256                mIsListener = resolvedType.findMethods(mName, isStatic) != null;
257                if (!mIsListener) {
258                    L.e("Could not find accessor %s.%s", resolvedType.getCanonicalName(), mName);
259                }
260                return modelAnalyzer.findClass(Object.class);
261            }
262
263            if (mGetter.isStatic() && !isStatic) {
264                // found a static method on an instance. register a new one
265                child.getParents().remove(this);
266                getChildren().remove(child);
267                StaticIdentifierExpr staticId = getModel().staticIdentifierFor(resolvedType);
268                getChildren().add(staticId);
269                staticId.getParents().add(this);
270                child = getChild(); // replace the child for the next if stmt
271            }
272
273            if (mGetter.resolvedType.isObservableField()) {
274                // Make this the ".get()" and add an extra field access for the observable field
275                child.getParents().remove(this);
276                getChildren().remove(child);
277
278                FieldAccessExpr observableField = getModel().observableField(child, mName);
279                observableField.mGetter = mGetter;
280
281                getChildren().add(observableField);
282                observableField.getParents().add(this);
283                mGetter = mGetter.resolvedType.findGetterOrField("", false);
284                mName = "";
285                mBrName = ExtKt.br(mName);
286            } else if (hasBindableAnnotations()) {
287                mBrName = ExtKt.br(BrNameUtil.brKey(mGetter));
288            }
289        }
290        return mGetter.resolvedType;
291    }
292
293    @Override
294    public Expr resolveTwoWayExpressions(Expr parent) {
295        final Expr child = getChild();
296        if (!(child instanceof ViewFieldExpr)) {
297            return this;
298        }
299        final ViewFieldExpr expr = (ViewFieldExpr) child;
300        final BindingTarget bindingTarget = expr.getBindingTarget();
301
302        // This is a binding to a View's attribute, so look for matching attribute
303        // on that View's BindingTarget. If there is an expression, we simply replace
304        // the binding with that binding expression.
305        for (Binding binding : bindingTarget.getBindings()) {
306            if (attributeMatchesName(binding.getName(), mName)) {
307                final Expr replacement = binding.getExpr();
308                replaceExpression(parent, replacement);
309                return replacement;
310            }
311        }
312
313        // There was no binding expression to bind to. This should be a two-way binding.
314        // This is a synthesized two-way binding because we must capture the events from
315        // the View and change the value when the target View's attribute changes.
316        final SetterStore setterStore = SetterStore.get(ModelAnalyzer.getInstance());
317        final ModelClass targetClass = expr.getResolvedType();
318        BindingGetterCall getter = setterStore.getGetterCall(mName, targetClass, null, null);
319        if (getter == null) {
320            getter = setterStore.getGetterCall("android:" + mName, targetClass, null, null);
321            if (getter == null) {
322                L.e("Could not resolve the two-way binding attribute '%s' on type '%s'",
323                        mName, targetClass);
324            }
325        }
326        InverseBinding inverseBinding = null;
327        for (Binding binding : bindingTarget.getBindings()) {
328            final Expr testExpr = binding.getExpr();
329            if (testExpr instanceof TwoWayListenerExpr &&
330                    getter.getEventAttribute().equals(binding.getName())) {
331                inverseBinding = ((TwoWayListenerExpr) testExpr).mInverseBinding;
332                break;
333            }
334        }
335        if (inverseBinding == null) {
336            inverseBinding = bindingTarget.addInverseBinding(mName, getter);
337        }
338        inverseBinding.addChainedExpression(this);
339        mIsViewAttributeAccess = true;
340        enableDirectInvalidation();
341        return this;
342    }
343
344    private static boolean attributeMatchesName(String attribute, String field) {
345        int colonIndex = attribute.indexOf(':');
346        return attribute.substring(colonIndex + 1).equals(field);
347    }
348
349    private void replaceExpression(Expr parent, Expr replacement) {
350        if (parent != null) {
351            List<Expr> children = parent.getChildren();
352            int index;
353            while ((index = children.indexOf(this)) >= 0) {
354                children.set(index, replacement);
355                replacement.getParents().add(parent);
356            }
357            while (getParents().remove(parent)) {
358                // just remove all copies of parent.
359            }
360        }
361        if (getParents().isEmpty()) {
362            getModel().removeExpr(this);
363        }
364    }
365
366    @Override
367    protected String asPackage() {
368        String parentPackage = getChild().asPackage();
369        return parentPackage == null ? null : parentPackage + "." + mName;
370    }
371
372    @Override
373    protected KCode generateCode(boolean expand) {
374        KCode code = new KCode();
375        if (expand) {
376            String defaultValue = ModelAnalyzer.getInstance().getDefaultValue(
377                    getResolvedType().toJavaCode());
378            code.app("(", getChild().toCode(true))
379                    .app(" == null) ? ")
380                    .app(defaultValue)
381                    .app(" : ");
382        }
383        code.app("", getChild().toCode(expand)).app(".");
384        if (getGetter().type == Callable.Type.FIELD) {
385            return code.app(getGetter().name);
386        } else {
387            return code.app(getGetter().name).app("()");
388        }
389    }
390
391    @Override
392    public KCode toInverseCode(KCode value) {
393        if (mGetter.setterName == null) {
394            throw new IllegalStateException("There is no inverse for " + toCode().generate());
395        }
396        KCode castValue = new KCode("(").app(getResolvedType().toJavaCode() + ")(", value).app(")");
397        String type = getChild().getResolvedType().toJavaCode();
398        KCode code = new KCode("targetObj_.");
399        if (getGetter().type == Callable.Type.FIELD) {
400            code.app(getGetter().setterName).app(" = ", castValue).app(";");
401        } else {
402            code.app(getGetter().setterName).app("(", castValue).app(")").app(";");
403        }
404        return new KCode()
405                .app("final ")
406                .app(type)
407                .app(" targetObj_ = ", getChild().toCode(true))
408                .app(";")
409                .nl(new KCode("if (targetObj_ != null) {"))
410                .tab(code)
411                .nl(new KCode("}"));
412    }
413}
414