blob: d897f0a7d2b78d51a9a55ea8fca19c8787e016e9 [file] [log] [blame]
/*
* 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<Expr> 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;
}
}