/* * Copyright (C) 2015 The Android Open Source Project * * 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 android.databinding.tool.expr; import android.databinding.tool.Binding; import android.databinding.tool.BindingTarget; import android.databinding.tool.InverseBinding; import android.databinding.tool.ext.ExtKt; import android.databinding.tool.processing.ErrorMessages; import android.databinding.tool.processing.Scope; import android.databinding.tool.reflection.Callable; import android.databinding.tool.reflection.Callable.Type; import android.databinding.tool.reflection.ModelAnalyzer; import android.databinding.tool.reflection.ModelClass; import android.databinding.tool.store.SetterStore; import android.databinding.tool.store.SetterStore.BindingGetterCall; import android.databinding.tool.util.BrNameUtil; import android.databinding.tool.util.L; import android.databinding.tool.util.Preconditions; import android.databinding.tool.writer.KCode; import com.google.common.collect.Lists; import java.util.List; public class FieldAccessExpr extends MethodBaseExpr { // notification name for the field. Important when we map this to a method w/ different name String mBrName; Callable mGetter; boolean mIsListener; boolean mIsViewAttributeAccess; FieldAccessExpr(Expr parent, String name) { super(parent, name); mName = name; } public Callable getGetter() { if (mGetter == null) { getResolvedType(); } return mGetter; } @Override public String getInvertibleError() { if (getGetter() == null) { return "Listeners do not support two-way binding"; } if (mGetter.setterName == null) { return "Two-way binding cannot resolve a setter for " + getResolvedType().toJavaCode() + " property '" + mName + "'"; } if (!mGetter.isDynamic()) { return "Cannot change a final field in " + getResolvedType().toJavaCode() + " property " + mName; } return null; } public int getMinApi() { return mGetter == null ? 0 : mGetter.getMinApi(); } @Override public boolean isDynamic() { if (mGetter == null) { getResolvedType(); } if (mGetter == null || mGetter.type == Type.METHOD) { return true; } // if it is static final, gone if (getTarget().isDynamic()) { // if owner is dynamic, then we can be dynamic unless we are static final return !mGetter.isStatic() || mGetter.isDynamic(); } if (mIsViewAttributeAccess) { return true; // must be able to invalidate this } // if owner is NOT dynamic, we can be dynamic if an only if getter is dynamic return mGetter.isDynamic(); } public boolean hasBindableAnnotations() { return mGetter != null && mGetter.canBeInvalidated(); } @Override public Expr resolveListeners(ModelClass listener, Expr parent) { final ModelClass targetType = getTarget().getResolvedType(); if (getGetter() == null && (listener == null || !mIsListener)) { L.e("Could not resolve %s.%s as an accessor or listener on the attribute.", targetType.getCanonicalName(), mName); return this; } try { Expr listenerExpr = resolveListenersAsMethodReference(listener, parent); L.w("Method references using '.' is deprecated. Instead of '%s', use '%s::%s'", toString(), getTarget(), getName()); return listenerExpr; } catch (IllegalStateException e) { if (getGetter() == null) { L.e("%s", e.getMessage()); } return this; } } @Override protected String computeUniqueKey() { return join(mName, ".", getTarget().getUniqueKey()); } public String getBrName() { if (mIsListener) { return null; } try { Scope.enter(this); Preconditions.checkNotNull(mGetter, "cannot get br name before resolving the getter"); return mBrName; } finally { Scope.exit(); } } @Override protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) { if (mIsListener) { return modelAnalyzer.findClass(Object.class); } if (mGetter == null) { Expr target = getTarget(); target.getResolvedType(); boolean isStatic = target instanceof StaticIdentifierExpr; ModelClass resolvedType = target.getResolvedType(); L.d("resolving %s. Resolved class type: %s", this, resolvedType); mGetter = resolvedType.findGetterOrField(mName, isStatic); if (mGetter == null) { mIsListener = !resolvedType.findMethods(mName, isStatic).isEmpty(); if (!mIsListener) { L.e("Could not find accessor %s.%s", resolvedType.getCanonicalName(), mName); } return modelAnalyzer.findClass(Object.class); } if (mGetter.isStatic() && !isStatic) { // found a static method on an instance. register a new one replaceStaticIdentifier(resolvedType); target = getTarget(); } if (mGetter.resolvedType.isObservableField()) { // Make this the ".get()" and add an extra field access for the observable field target.getParents().remove(this); getChildren().remove(target); FieldAccessExpr observableField = getModel().observableField(target, mName); getChildren().add(observableField); observableField.getParents().add(this); mGetter = mGetter.resolvedType.findGetterOrField("", false); mName = ""; mBrName = ExtKt.br(mName); } else if (hasBindableAnnotations()) { mBrName = ExtKt.br(BrNameUtil.brKey(mGetter)); } } return mGetter.resolvedType; } protected void replaceStaticIdentifier(ModelClass staticIdentifierType) { getTarget().getParents().remove(this); getChildren().remove(getTarget()); StaticIdentifierExpr staticId = getModel().staticIdentifierFor(staticIdentifierType); getChildren().add(staticId); staticId.getParents().add(this); } @Override public Expr resolveTwoWayExpressions(Expr parent) { final Expr child = getTarget(); if (!(child instanceof ViewFieldExpr)) { return this; } final ViewFieldExpr expr = (ViewFieldExpr) child; final BindingTarget bindingTarget = expr.getBindingTarget(); // This is a binding to a View's attribute, so look for matching attribute // on that View's BindingTarget. If there is an expression, we simply replace // the binding with that binding expression. for (Binding binding : bindingTarget.getBindings()) { if (attributeMatchesName(binding.getName(), mName)) { final Expr replacement = binding.getExpr(); replaceExpression(parent, replacement); return replacement; } } // There was no binding expression to bind to. This should be a two-way binding. // This is a synthesized two-way binding because we must capture the events from // the View and change the value when the target View's attribute changes. final SetterStore setterStore = SetterStore.get(ModelAnalyzer.getInstance()); final ModelClass targetClass = expr.getResolvedType(); BindingGetterCall getter = setterStore.getGetterCall(mName, targetClass, null, null); if (getter == null) { getter = setterStore.getGetterCall("android:" + mName, targetClass, null, null); if (getter == null) { L.e("Could not resolve the two-way binding attribute '%s' on type '%s'", mName, targetClass); } } InverseBinding inverseBinding = null; for (Binding binding : bindingTarget.getBindings()) { final Expr testExpr = binding.getExpr(); if (testExpr instanceof TwoWayListenerExpr && getter.getEventAttribute().equals(binding.getName())) { inverseBinding = ((TwoWayListenerExpr) testExpr).mInverseBinding; break; } } if (inverseBinding == null) { inverseBinding = bindingTarget.addInverseBinding(mName, getter); } inverseBinding.addChainedExpression(this); mIsViewAttributeAccess = true; enableDirectInvalidation(); return this; } private static boolean attributeMatchesName(String attribute, String field) { int colonIndex = attribute.indexOf(':'); return attribute.substring(colonIndex + 1).equals(field); } private void replaceExpression(Expr parent, Expr replacement) { if (parent != null) { List children = parent.getChildren(); int index; while ((index = children.indexOf(this)) >= 0) { children.set(index, replacement); replacement.getParents().add(parent); } while (getParents().remove(parent)) { // just remove all copies of parent. } } if (getParents().isEmpty()) { getModel().removeExpr(this); } } @Override protected String asPackage() { String parentPackage = getTarget().asPackage(); return parentPackage == null ? null : parentPackage + "." + mName; } @Override protected KCode generateCode() { // once we can deprecate using Field.access for callbacks, we can get rid of this since // it will be detected when resolve type is run. Preconditions.checkNotNull(getGetter(), ErrorMessages.CANNOT_RESOLVE_TYPE, this); KCode code = new KCode() .app("", getTarget().toCode()).app("."); if (getGetter().type == Callable.Type.FIELD) { return code.app(getGetter().name); } else { return code.app(getGetter().name).app("()"); } } @Override public Expr generateInverse(ExprModel model, Expr value, String bindingClassName) { Expr castExpr = model.castExpr(getResolvedType().toJavaCode(), value); Expr target = getTarget().cloneToModel(model); Expr result; if (getGetter().type == Callable.Type.FIELD) { result = model.assignment(target, mName, castExpr); } else { result = model.methodCall(target, mGetter.setterName, Lists.newArrayList(castExpr)); } return result; } @Override public Expr cloneToModel(ExprModel model) { final Expr clonedTarget = getTarget().cloneToModel(model); return model.field(clonedTarget, mName); } @Override public String toString() { String name = mName.isEmpty() ? "get()" : mName; return getTarget().toString() + '.' + name; } }