From ac5dc9a4e13600d20b4e0ec1ecab083a41f966b3 Mon Sep 17 00:00:00 2001 From: Yigit Boyar Date: Tue, 3 Mar 2015 18:58:24 -0800 Subject: [PATCH] Check API version for methods called via binding In data binding, setting an attribute actually means calling a method, which might be an issue if the method is added after a certain API. This CL introduces a change which will check called methods per api and add necessary API check code to avoid calling those methods in older platforms. This CL also resurrects the Java Model Analyzer (in testing) and also fixes compiler tests. Bug: 19593398 Change-Id: I0da4194625231cf43125e1b43338069e7d191eb9 --- .../library/DataBinderTrojan.java} | 13 +- .../databinding/testapp/NewApiTest.java | 82 ++++ .../databinding/testapp/TestActivity.java | 3 + .../src/main/res/layout/new_api_layout.xml | 27 ++ .../ProcessExpressions.java | 21 +- .../main/java/android/binding/Bindable.java | 1 + .../java/android/binding/BindingAppInfo.java | 2 + tools/data-binding/compiler/build.gradle | 1 + .../java/com/android/databinding/Binding.java | 27 +- .../com/android/databinding/DataBinder.java | 1 + .../databinding/LayoutXmlProcessor.java | 19 +- .../databinding/reflection/ModelAnalyzer.java | 66 +++- .../databinding/reflection/ModelClass.java | 17 + .../databinding/reflection/ModelMethod.java | 15 + .../databinding/reflection/SdkUtil.java | 157 ++++++++ .../databinding/reflection/TypeUtil.java | 57 +++ .../{ => annotation}/AnnotationAnalyzer.java | 100 +++-- .../{ => annotation}/AnnotationClass.java | 47 ++- .../{ => annotation}/AnnotationField.java | 8 +- .../{ => annotation}/AnnotationMethod.java | 36 +- .../annotation/AnnotationTypeUtil.java | 105 +++++ .../databinding/store/SetterStore.java | 202 ++++++---- .../java/com/android/databinding/util/L.java | 37 +- .../databinding/writer/LayoutBinderWriter.kt | 11 +- .../databinding/ExpressionVisitorTest.java | 14 +- .../android/databinding/LayoutBinderTest.java | 8 +- .../databinding/MockIViewDataBinder.java | 18 - ...ervableLsit.java => MockLayoutBinder.java} | 8 +- .../databinding/MockObservableMap.java | 18 - .../android/databinding/SdkVersionTest.java | 49 +++ .../databinding/expr/ExprModelTest.java | 30 +- .../android/databinding/expr/ExprTest.java | 12 +- .../reflection/java/JavaAnalyzer.java | 361 ++++++++++++++++++ .../reflection/java/JavaClass.java | 245 ++++++++++++ .../java/JavaField.java} | 15 +- .../reflection/java/JavaMethod.java | 78 ++++ .../reflection/java/JavaTypeUtil.java | 89 +++++ .../gradlePlugin/src/main/kotlin/plugin.kt | 12 +- tools/data-binding/library/build.gradle | 15 +- .../databinding/library/DataBinder.java | 11 + 40 files changed, 1781 insertions(+), 257 deletions(-) rename tools/data-binding/{compiler/src/test/java/com/android/databinding/MockObservable.java => TestApp/src/androidTest/java/com/android/databinding/library/DataBinderTrojan.java} (69%) create mode 100644 tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/testapp/NewApiTest.java create mode 100644 tools/data-binding/TestApp/src/main/res/layout/new_api_layout.xml create mode 100644 tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/SdkUtil.java create mode 100644 tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/TypeUtil.java rename tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/{ => annotation}/AnnotationAnalyzer.java (85%) rename tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/{ => annotation}/AnnotationClass.java (88%) rename tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/{ => annotation}/AnnotationField.java (88%) rename tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/{ => annotation}/AnnotationMethod.java (70%) create mode 100644 tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationTypeUtil.java delete mode 100644 tools/data-binding/compiler/src/test/java/com/android/databinding/MockIViewDataBinder.java rename tools/data-binding/compiler/src/test/java/com/android/databinding/{MockObservableLsit.java => MockLayoutBinder.java} (71%) delete mode 100644 tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservableMap.java create mode 100644 tools/data-binding/compiler/src/test/java/com/android/databinding/SdkVersionTest.java create mode 100644 tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaAnalyzer.java create mode 100644 tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaClass.java rename tools/data-binding/compiler/src/test/java/com/android/databinding/{MockBindable.java => reflection/java/JavaField.java} (68%) create mode 100644 tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaMethod.java create mode 100644 tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaTypeUtil.java diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservable.java b/tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/library/DataBinderTrojan.java similarity index 69% rename from tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservable.java rename to tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/library/DataBinderTrojan.java index f7e36508a05d3..eeba4f2468bad 100644 --- a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservable.java +++ b/tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/library/DataBinderTrojan.java @@ -1,9 +1,12 @@ /* * 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. @@ -11,11 +14,13 @@ * limitations under the License. */ -package com.android.databinding; +package com.android.databinding.library; /** - * Mock class for Observable interface, used for testing. + * This helper is used to toggle DataBinder's package private values to change behavior for testing */ -public class MockObservable { - +public class DataBinderTrojan { + public static void setBuildSdkInt(int level) { + DataBinder.SDK_INT = level; + } } diff --git a/tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/testapp/NewApiTest.java b/tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/testapp/NewApiTest.java new file mode 100644 index 0000000000000..9b28237f41bc8 --- /dev/null +++ b/tools/data-binding/TestApp/src/androidTest/java/com/android/databinding/testapp/NewApiTest.java @@ -0,0 +1,82 @@ +/* + * 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 com.android.databinding.testapp; + +import com.android.databinding.library.DataBinderTrojan; +import com.android.databinding.testapp.generated.NewApiLayoutBinder; + +import android.content.Context; +import android.os.Build; +import android.test.UiThreadTest; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.ArrayList; + +public class NewApiTest extends BaseDataBinderTest { + public NewApiTest() { + super(NewApiLayoutBinder.class, R.layout.new_api_layout); + } + + @UiThreadTest + public void testSetElevation() { + mBinder.setElevation(3); + mBinder.setName("foo"); + mBinder.setChildren(new ArrayList()); + mBinder.rebindDirty(); + assertEquals("foo", mBinder.getTextView().getText().toString()); + assertEquals(3f, mBinder.getTextView().getElevation()); + } + + @UiThreadTest + public void testSetElevationOlderAPI() { + DataBinderTrojan.setBuildSdkInt(1); + try { + TextView textView = mBinder.getTextView(); + float originalElevation = textView.getElevation(); + mBinder.setElevation(3); + mBinder.setName("foo2"); + mBinder.rebindDirty(); + assertEquals("foo2", textView.getText().toString()); + assertEquals(originalElevation, textView.getElevation()); + } finally { + DataBinderTrojan.setBuildSdkInt(Build.VERSION.SDK_INT); + } + } + + @UiThreadTest + public void testGeneric() { + ArrayList views = new ArrayList<>(); + mBinder.setChildren(views); + mBinder.rebindDirty(); + assertEquals(1, views.size()); + assertSame(mBinder.getTextView(), views.get(0)); + } + + @UiThreadTest + public void testGenericOlderApi() { + DataBinderTrojan.setBuildSdkInt(1); + try { + ArrayList views = new ArrayList<>(); + mBinder.setChildren(views); + mBinder.rebindDirty(); + // we should not call the api on older platforms. + assertEquals(0, views.size()); + } finally { + DataBinderTrojan.setBuildSdkInt(Build.VERSION.SDK_INT); + } + } +} diff --git a/tools/data-binding/TestApp/src/main/java/com/android/databinding/testapp/TestActivity.java b/tools/data-binding/TestApp/src/main/java/com/android/databinding/testapp/TestActivity.java index c5a5b17c8e47d..fa9dd42ccd0f1 100644 --- a/tools/data-binding/TestApp/src/main/java/com/android/databinding/testapp/TestActivity.java +++ b/tools/data-binding/TestApp/src/main/java/com/android/databinding/testapp/TestActivity.java @@ -1,9 +1,12 @@ /* * 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. diff --git a/tools/data-binding/TestApp/src/main/res/layout/new_api_layout.xml b/tools/data-binding/TestApp/src/main/res/layout/new_api_layout.xml new file mode 100644 index 0000000000000..686676b0db272 --- /dev/null +++ b/tools/data-binding/TestApp/src/main/res/layout/new_api_layout.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/tools/data-binding/annotationprocessor/src/main/java/com/android/databinding/annotationprocessor/ProcessExpressions.java b/tools/data-binding/annotationprocessor/src/main/java/com/android/databinding/annotationprocessor/ProcessExpressions.java index e12ad3e5b841f..2b047bb260973 100644 --- a/tools/data-binding/annotationprocessor/src/main/java/com/android/databinding/annotationprocessor/ProcessExpressions.java +++ b/tools/data-binding/annotationprocessor/src/main/java/com/android/databinding/annotationprocessor/ProcessExpressions.java @@ -1,7 +1,25 @@ +/* + * 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 com.android.databinding.annotationprocessor; import com.android.databinding.CompilerChef; +import com.android.databinding.reflection.SdkUtil; import com.android.databinding.store.ResourceBundle; +import com.android.databinding.util.L; import com.android.databinding.writer.AnnotationJavaFileWriter; import org.apache.commons.codec.binary.Base64; @@ -11,6 +29,7 @@ import android.binding.BinderBundle; import android.binding.BindingAppInfo; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; @@ -37,12 +56,12 @@ public class ProcessExpressions extends AbstractProcessor { public boolean process(Set annotations, RoundEnvironment roundEnv) { ResourceBundle resourceBundle = null; - for (Element element : roundEnv.getElementsAnnotatedWith(BindingAppInfo.class)) { final BindingAppInfo appInfo = element.getAnnotation(BindingAppInfo.class); if (appInfo == null) { continue; // It gets confused between BindingAppInfo and BinderBundle } + SdkUtil.initialize(appInfo.minSdk(), new File(appInfo.sdkRoot())); if (element.getKind() != ElementKind.CLASS) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "BindingAppInfo associated with wrong type. Should be a class.", element); diff --git a/tools/data-binding/baseLibrary/src/main/java/android/binding/Bindable.java b/tools/data-binding/baseLibrary/src/main/java/android/binding/Bindable.java index 80464b453b101..09bcde9f04588 100644 --- a/tools/data-binding/baseLibrary/src/main/java/android/binding/Bindable.java +++ b/tools/data-binding/baseLibrary/src/main/java/android/binding/Bindable.java @@ -21,5 +21,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) // this is necessary for java analyzer to work public @interface Bindable { } diff --git a/tools/data-binding/baseLibrary/src/main/java/android/binding/BindingAppInfo.java b/tools/data-binding/baseLibrary/src/main/java/android/binding/BindingAppInfo.java index 42a6f3d1d76d9..a90f33285a54b 100644 --- a/tools/data-binding/baseLibrary/src/main/java/android/binding/BindingAppInfo.java +++ b/tools/data-binding/baseLibrary/src/main/java/android/binding/BindingAppInfo.java @@ -25,4 +25,6 @@ import java.lang.annotation.Target; public @interface BindingAppInfo { String buildId(); String applicationPackage(); + String sdkRoot(); + int minSdk(); } diff --git a/tools/data-binding/compiler/build.gradle b/tools/data-binding/compiler/build.gradle index 96917aecff9ef..8ebb27af6c1b6 100644 --- a/tools/data-binding/compiler/build.gradle +++ b/tools/data-binding/compiler/build.gradle @@ -40,6 +40,7 @@ dependencies { compile project(":baseLibrary") compile project(":grammarBuilder") compile project(":xmlGrammar") + testCompile "com.android.databinding:library:$version@jar" } uploadArchives { repositories { diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/Binding.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/Binding.java index 9ccf18805cab7..4425e0fddb229 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/Binding.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/Binding.java @@ -18,6 +18,7 @@ package com.android.databinding; import com.android.databinding.reflection.ModelAnalyzer; import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.SdkUtil; import com.android.databinding.store.SetterStore; import com.android.databinding.expr.Expr; @@ -26,7 +27,7 @@ public class Binding { private final String mName; private final Expr mExpr; private final BindingTarget mTarget; - private String mJavaCode = null; + private SetterStore.SetterCall mSetterCall; public Binding(BindingTarget target, String name, Expr expr) { mTarget = target; @@ -34,15 +35,31 @@ public class Binding { mExpr = expr; } + private SetterStore.SetterCall getSetterCall() { + if (mSetterCall == null) { + ModelClass viewType = mTarget.getResolvedType(); + mSetterCall = SetterStore.get(ModelAnalyzer.getInstance()).getSetterCall(mName, + viewType, mExpr.getResolvedType(), mExpr.getModel().getImports()); + } + return mSetterCall; + } + public BindingTarget getTarget() { return mTarget; } public String toJavaCode(String targetViewName, String expressionCode) { - ModelClass viewType = mTarget.getResolvedType(); - return SetterStore.get(ModelAnalyzer.getInstance()).getSetterCall(mName, viewType, - mExpr.getResolvedType(), targetViewName, expressionCode, - mExpr.getModel().getImports()); + return getSetterCall().toJava(targetViewName, expressionCode); + } + + /** + * The min api level in which this binding should be executed. + *

+ * This should be the minimum value among the dependencies of this binding. For now, we only + * check the setter. + */ + public int getMinApi() { + return getSetterCall().getMinApi(); } // private String resolveJavaCode(ModelAnalyzer modelAnalyzer) { diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/DataBinder.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/DataBinder.java index fc29b9e84ed02..1514a9e1cb5eb 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/DataBinder.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/DataBinder.java @@ -62,6 +62,7 @@ public class DataBinder { public void writeBinders() { for (LayoutBinder layoutBinder : mLayoutBinders) { + L.d("writing data binder %s", layoutBinder.getClassName()); mFileWriter.writeToFile(layoutBinder.getPackage() + "." + layoutBinder.getClassName(), layoutBinder.writeViewBinder()); } diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/LayoutXmlProcessor.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/LayoutXmlProcessor.java index cb4860e5d392d..9bca851d09aa1 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/LayoutXmlProcessor.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/LayoutXmlProcessor.java @@ -21,6 +21,7 @@ import com.android.databinding.store.ResourceBundle; import com.android.databinding.writer.JavaFileWriter; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringEscapeUtils; import org.xml.sax.SAXException; import java.io.File; @@ -48,16 +49,19 @@ public class LayoutXmlProcessor { public static final String APPLICATION_INFO_CLASS = "ApplicationBindingInfo"; private final JavaFileWriter mFileWriter; private final ResourceBundle mResourceBundle; + private final int mMinSdk; + private boolean mProcessingComplete; private boolean mWritten; private final String mBuildId = UUID.randomUUID().toString(); private final List mResourceFolders; public LayoutXmlProcessor(String applicationPackage, List resourceFolders, - JavaFileWriter fileWriter) { + JavaFileWriter fileWriter, int minSdk) { mFileWriter = fileWriter; mResourceBundle = new ResourceBundle(applicationPackage); mResourceFolders = resourceFolders; + mMinSdk = minSdk; } public boolean processResources() @@ -84,13 +88,13 @@ public class LayoutXmlProcessor { return true; } - public void writeIntermediateFile() throws JAXBException { + public void writeIntermediateFile(File sdkDir) throws JAXBException { if (mWritten) { return; } JAXBContext context = JAXBContext.newInstance(ResourceBundle.LayoutFileBundle.class); Marshaller marshaller = context.createMarshaller(); - writeAppInfo(marshaller); + writeAppInfo(marshaller, sdkDir); for (List layouts : mResourceBundle.getLayoutBundles() .values()) { for (ResourceBundle.LayoutFileBundle layout : layouts) { @@ -124,10 +128,13 @@ public class LayoutXmlProcessor { mFileWriter.writeToFile(RESOURCE_BUNDLE_PACKAGE + className, classString); } - private void writeAppInfo(Marshaller marshaller) { + private void writeAppInfo(Marshaller marshaller, File sdkDir) { + final String sdkPath = StringEscapeUtils.escapeJava(sdkDir.getAbsolutePath()); String classString = "import android.binding.BindingAppInfo;\n\n" + - "@BindingAppInfo(buildId=\"" + mBuildId + "\", applicationPackage=\"" + - mResourceBundle.getAppPackage() + "\")\n" + + "@BindingAppInfo(buildId=\"" + mBuildId + "\", " + + "applicationPackage=\"" + mResourceBundle.getAppPackage() + "\", " + + "sdkRoot=\"" + sdkPath + "\", " + + "minSdk=" + mMinSdk + ")\n" + "public class " + APPLICATION_INFO_CLASS + " {}\n"; mFileWriter.writeToFile(RESOURCE_BUNDLE_PACKAGE + APPLICATION_INFO_CLASS, classString); } diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelAnalyzer.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelAnalyzer.java index eb40113b2ff02..b0e66c8b681c8 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelAnalyzer.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelAnalyzer.java @@ -16,16 +16,61 @@ package com.android.databinding.reflection; import com.google.common.base.Preconditions; +import com.android.databinding.reflection.annotation.AnnotationAnalyzer; +import com.android.databinding.util.L; import java.net.URL; import java.util.List; import java.util.Map; - import javax.annotation.processing.ProcessingEnvironment; public abstract class ModelAnalyzer { + + public static final String[] LIST_CLASS_NAMES = { + "java.util.List", + "android.util.SparseArray", + "android.util.SparseBooleanArray", + "android.util.SparseIntArray", + "android.util.SparseLongArray", + "android.util.LongSparseArray", + "android.support.v4.util.LongSparseArray", + }; + + public static final String MAP_CLASS_NAME = "java.util.Map"; + + public static final String STRING_CLASS_NAME = "java.lang.String"; + + public static final String OBJECT_CLASS_NAME = "java.lang.Object"; + + static ModelAnalyzer instance; + + public static final String OBSERVABLE_CLASS_NAME = "android.binding.Observable"; + + public static final String OBSERVABLE_LIST_CLASS_NAME = "android.binding.ObservableList"; + + public static final String OBSERVABLE_MAP_CLASS_NAME = "android.binding.ObservableMap"; + + public static final String[] OBSERVABLE_FIELDS = { + "com.android.databinding.library.ObservableBoolean", + "com.android.databinding.library.ObservableByte", + "com.android.databinding.library.ObservableChar", + "com.android.databinding.library.ObservableShort", + "com.android.databinding.library.ObservableInt", + "com.android.databinding.library.ObservableLong", + "com.android.databinding.library.ObservableFloat", + "com.android.databinding.library.ObservableDouble", + "com.android.databinding.library.ObservableField", + }; + + public static final String I_VIEW_DATA_BINDER + = "com.android.databinding.library.IViewDataBinder"; + private static ModelAnalyzer sAnalyzer; + protected void setInstance(ModelAnalyzer analyzer) { + sAnalyzer = analyzer; + } + public abstract boolean isDataBinder(ModelClass modelClass); public abstract Callable findMethod(ModelClass modelClass, String name, @@ -68,27 +113,32 @@ public abstract class ModelAnalyzer { } public static void setProcessingEnvironment(ProcessingEnvironment processingEnvironment) { + if (sAnalyzer != null) { + throw new IllegalStateException("processing env is already created, you cannot " + + "change class loader after that"); + } + L.d("setting processing env to %s", processingEnvironment); AnnotationAnalyzer annotationAnalyzer = new AnnotationAnalyzer(processingEnvironment); sAnalyzer = annotationAnalyzer; } public String getDefaultValue(String className) { - if("int".equals(className)) { + if ("int".equals(className)) { return "0"; } - if("short".equals(className)) { + if ("short".equals(className)) { return "0"; } - if("long".equals(className)) { + if ("long".equals(className)) { return "0L"; } - if("float".equals(className)) { + if ("float".equals(className)) { return "0f"; } - if("double".equals(className)) { + if ("double".equals(className)) { return "0.0"; } - if("boolean".equals(className)) { + if ("boolean".equals(className)) { return "false"; } if ("char".equals(className)) { @@ -105,4 +155,6 @@ public abstract class ModelAnalyzer { public abstract List getResources(String name); public abstract ModelClass findClass(Class classType); + + public abstract TypeUtil createTypeUtil(); } diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelClass.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelClass.java index 6e5f41bcda144..401153571680c 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelClass.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelClass.java @@ -62,4 +62,21 @@ public interface ModelClass { ModelMethod[] getMethods(String name, int numParameters); ModelClass getSuperclass(); + + String getCanonicalName(); + + /** + * Since when this class is available. Important for Binding expressions so that we don't + * call non-existing APIs when setting UI. + * + * @return The SDK_INT where this method was added. If it is not a framework method, should + * return 1. + */ + int getMinApi(); + + /** + * Returns the JNI description of the method which can be used to lookup it in SDK. + * @see com.android.databinding.reflection.TypeUtil + */ + String getJniDescription(); } diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelMethod.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelMethod.java index 822f5989dd596..cea57c79329a2 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelMethod.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/ModelMethod.java @@ -29,4 +29,19 @@ public interface ModelMethod { boolean isPublic(); boolean isStatic(); + + /** + * Since when this method is available. Important for Binding expressions so that we don't + * call non-existing APIs when setting UI. + * + * @return The SDK_INT where this method was added. If it is not a framework method, should + * return 1. + */ + int getMinApi(); + + /** + * Returns the JNI description of the method which can be used to lookup it in SDK. + * @see com.android.databinding.reflection.TypeUtil + */ + String getJniDescription(); } diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/SdkUtil.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/SdkUtil.java new file mode 100644 index 0000000000000..6efe2bd2b4e93 --- /dev/null +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/SdkUtil.java @@ -0,0 +1,157 @@ +/* + * 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 com.android.databinding.reflection; + +import com.google.common.base.Preconditions; +import com.android.databinding.util.L; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +/** + * Class that is used for SDK related stuff. + *

+ * Must be initialized with the sdk location to work properly + */ +public class SdkUtil { + + static File mSdkPath; + + static ApiChecker mApiChecker; + + static int mMinSdk; + + public static void initialize(int minSdk, File sdkPath) { + mSdkPath = sdkPath; + mMinSdk = minSdk; + mApiChecker = new ApiChecker(new File(sdkPath.getAbsolutePath() + + "/platform-tools/api/api-versions.xml")); + L.d("SdkUtil init, minSdk: %s", minSdk); + } + + public static int getMinApi(ModelClass modelClass) { + return mApiChecker.getMinApi(modelClass.getJniDescription(), null); + } + + public static int getMinApi(ModelMethod modelMethod) { + ModelClass declaringClass = modelMethod.getDeclaringClass(); + Preconditions.checkNotNull(mApiChecker, "should've initialized api checker"); + while (declaringClass != null) { + String classDesc = declaringClass.getJniDescription(); + String methodDesc = modelMethod.getJniDescription(); + int result = mApiChecker.getMinApi(classDesc, methodDesc); + L.d("checking method api for %s, class:%s method:%s. result: %d", modelMethod.getName(), + classDesc, methodDesc, result); + if (result > 1) { + return result; + } + declaringClass = declaringClass.getSuperclass(); + } + return 1; + } + + private static class ApiChecker { + + private Map mFullLookup = new HashMap<>(); + + private Document mDoc; + + private XPath mXPath; + + public ApiChecker(File apiFile) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + mDoc = builder.parse(apiFile); + XPathFactory xPathFactory = XPathFactory.newInstance(); + mXPath = xPathFactory.newXPath(); + buildFullLookup(); + } catch (Throwable t) { + L.e(t, "cannot load api descriptions from %s", apiFile); + } + } + + private void buildFullLookup() throws XPathExpressionException { + NodeList allClasses = mDoc.getChildNodes().item(0).getChildNodes(); + for (int j = 0; j < allClasses.getLength(); j++) { + Node node = allClasses.item(j); + if (node.getNodeType() != Node.ELEMENT_NODE || !"class" + .equals(node.getNodeName())) { + continue; + } + //L.d("checking node %s", node.getAttributes().getNamedItem("name").getNodeValue()); + int classSince = getSince(node); + String classDesc = node.getAttributes().getNamedItem("name").getNodeValue(); + + final NodeList childNodes = node.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node child = childNodes.item(i); + if (child.getNodeType() != Node.ELEMENT_NODE || !"method" + .equals(child.getNodeName())) { + continue; + } + int methodSince = getSince(child); + int since = Math.max(classSince, methodSince); + if (since > SdkUtil.mMinSdk) { + String methodDesc = child.getAttributes().getNamedItem("name") + .getNodeValue(); + String key = cacheKey(classDesc, methodDesc); + L.d("adding method lookup %s as %s", key, since); + mFullLookup.put(key, since); + } + } + } + } + + public int getMinApi(String classDesc, String methodOrFieldDesc) { + if (mDoc == null || mXPath == null) { + return 1; + } + if (classDesc == null || classDesc.isEmpty()) { + return 1; + } + final String key = cacheKey(classDesc, methodOrFieldDesc); + Integer since = mFullLookup.get(key); + return since == null ? 1 : since; + } + + private static String cacheKey(String classDesc, String methodOrFieldDesc) { + return classDesc + "~" + methodOrFieldDesc; + } + + private static int getSince(Node node) { + final Node since = node.getAttributes().getNamedItem("since"); + if (since != null) { + final String nodeValue = since.getNodeValue(); + if (nodeValue != null && !nodeValue.isEmpty()) { + try { + return Integer.parseInt(nodeValue); + } catch (Throwable t) { + } + } + } + + return 1; + } + } +} diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/TypeUtil.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/TypeUtil.java new file mode 100644 index 0000000000000..50ae3c2e5b076 --- /dev/null +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/TypeUtil.java @@ -0,0 +1,57 @@ +/* + * 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 com.android.databinding.reflection; + +public abstract class TypeUtil { + + public static final String BYTE = "B"; + + public static final String CHAR = "C"; + + public static final String DOUBLE = "D"; + + public static final String FLOAT = "F"; + + public static final String INT = "I"; + + public static final String LONG = "J"; + + public static final String SHORT = "S"; + + public static final String VOID = "V"; + + public static final String BOOLEAN = "Z"; + + public static final String ARRAY = "["; + + public static final String CLASS_PREFIX = "L"; + + public static final String CLASS_SUFFIX = ";"; + + private static TypeUtil sInstance; + + abstract public String getDescription(ModelClass modelClass); + + abstract public String getDescription(ModelMethod modelMethod); + + public static TypeUtil getInstance() { + if (sInstance == null) { + sInstance = ModelAnalyzer.getInstance().createTypeUtil(); + } + return sInstance; + } +} diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationAnalyzer.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationAnalyzer.java similarity index 85% rename from tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationAnalyzer.java rename to tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationAnalyzer.java index 6c1dd76df731d..d3e32ed899a29 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationAnalyzer.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationAnalyzer.java @@ -13,10 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.databinding.reflection; +package com.android.databinding.reflection.annotation; import com.google.common.collect.ImmutableMap; +import com.android.databinding.reflection.Callable; +import com.android.databinding.reflection.ModelAnalyzer; +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.ModelField; +import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.reflection.TypeUtil; import com.android.databinding.util.L; import org.apache.commons.lang3.StringUtils; @@ -45,39 +51,7 @@ import javax.tools.Diagnostic; public class AnnotationAnalyzer extends ModelAnalyzer { - public static final String[] LIST_CLASS_NAMES = { - "java.util.List", - "android.util.SparseArray", - "android.util.SparseBooleanArray", - "android.util.SparseIntArray", - "android.util.SparseLongArray", - "android.util.LongSparseArray", - "android.support.v4.util.LongSparseArray", - }; - - public static final String MAP_CLASS_NAME = "java.util.Map"; - - public static final String STRING_CLASS_NAME = "java.lang.String"; - - public static final String OBJECT_CLASS_NAME = "java.lang.Object"; - - static AnnotationAnalyzer instance; - private static final String OBSERVABLE_CLASS_NAME = "android.binding.Observable"; - private static final String OBSERVABLE_LIST_CLASS_NAME = "android.binding.ObservableList"; - private static final String OBSERVABLE_MAP_CLASS_NAME = "android.binding.ObservableMap"; - private static final String[] OBSERVABLE_FIELDS = { - "com.android.databinding.library.ObservableBoolean", - "com.android.databinding.library.ObservableByte", - "com.android.databinding.library.ObservableChar", - "com.android.databinding.library.ObservableShort", - "com.android.databinding.library.ObservableInt", - "com.android.databinding.library.ObservableLong", - "com.android.databinding.library.ObservableFloat", - "com.android.databinding.library.ObservableDouble", - "com.android.databinding.library.ObservableField", - }; - private static final String I_VIEW_DATA_BINDER = "com.android.databinding.library.IViewDataBinder"; - private static final Map PRIMITIVE_TYPES = + public static final Map PRIMITIVE_TYPES = new ImmutableMap.Builder() .put("boolean", TypeKind.BOOLEAN) .put("byte", TypeKind.BYTE) @@ -89,7 +63,7 @@ public class AnnotationAnalyzer extends ModelAnalyzer { .put("double", TypeKind.DOUBLE) .build(); - public final ProcessingEnvironment processingEnv; + public final ProcessingEnvironment mProcessingEnv; private AnnotationClass[] mListTypes; private AnnotationClass mMapType; @@ -102,8 +76,8 @@ public class AnnotationAnalyzer extends ModelAnalyzer { private AnnotationClass mIViewDataBinderType; public AnnotationAnalyzer(ProcessingEnvironment processingEnvironment) { - processingEnv = processingEnvironment; - instance = this; + mProcessingEnv = processingEnvironment; + setInstance(this); } public AnnotationClass[] getListTypes() { @@ -120,6 +94,10 @@ public class AnnotationAnalyzer extends ModelAnalyzer { return mListTypes; } + public static AnnotationAnalyzer get() { + return (AnnotationAnalyzer) getInstance(); + } + public AnnotationClass getMapType() { if (mMapType == null) { mMapType = loadClassErasure(MAP_CLASS_NAME); @@ -154,7 +132,7 @@ public class AnnotationAnalyzer extends ModelAnalyzer { } return mObservableListType; } - + private AnnotationClass getObservableMapType() { if (mObservableMapType == null) { mObservableMapType = loadClassErasure(OBSERVABLE_MAP_CLASS_NAME); @@ -185,7 +163,7 @@ public class AnnotationAnalyzer extends ModelAnalyzer { } private TypeElement findType(String type) { - return processingEnv.getElementUtils().getTypeElement(type); + return mProcessingEnv.getElementUtils().getTypeElement(type); } @Override @@ -196,9 +174,9 @@ public class AnnotationAnalyzer extends ModelAnalyzer { @Override public Callable findMethod(ModelClass modelClass, String name, List args, boolean staticAccess) { - AnnotationClass clazz = (AnnotationClass)modelClass; + AnnotationClass clazz = (AnnotationClass) modelClass; // TODO implement properly - for (String methodName : new String[]{"set" + StringUtils.capitalize(name), name}) { + for (String methodName : new String[]{"set" + StringUtils.capitalize(name), name}) { for (ModelMethod method : clazz.getMethods(methodName, args.size())) { if (method.isStatic() == staticAccess) { ModelClass[] parameters = method.getParameterTypes(); @@ -226,13 +204,14 @@ public class AnnotationAnalyzer extends ModelAnalyzer { } } String message = "cannot find method '" + name + "' in class " + clazz.toJavaCode(); - printMessage(Diagnostic.Kind.ERROR, message); - throw new IllegalArgumentException(message); + IllegalArgumentException e = new IllegalArgumentException(message); + L.e(e, "cannot find method %s in class %s", name, clazz.toJavaCode()); + throw e; } @Override public boolean isObservable(ModelClass modelClass) { - AnnotationClass annotationClass = (AnnotationClass)modelClass; + AnnotationClass annotationClass = (AnnotationClass) modelClass; return getObservableType().isAssignableFrom(annotationClass) || getObservableListType().isAssignableFrom(annotationClass) || getObservableMapType().isAssignableFrom(annotationClass); @@ -240,8 +219,9 @@ public class AnnotationAnalyzer extends ModelAnalyzer { @Override public boolean isObservableField(ModelClass modelClass) { - AnnotationClass annotationClass = (AnnotationClass)modelClass; - AnnotationClass erasure = new AnnotationClass(getTypeUtils().erasure(annotationClass.mTypeMirror)); + AnnotationClass annotationClass = (AnnotationClass) modelClass; + AnnotationClass erasure = new AnnotationClass( + getTypeUtils().erasure(annotationClass.mTypeMirror)); for (AnnotationClass observableField : getObservableFieldTypes()) { if (observableField.isAssignableFrom(erasure)) { return true; @@ -264,7 +244,7 @@ public class AnnotationAnalyzer extends ModelAnalyzer { @Override public Callable findMethodOrField(ModelClass modelClass, String name, boolean staticAccess) { - AnnotationClass annotationClass = (AnnotationClass)modelClass; + AnnotationClass annotationClass = (AnnotationClass) modelClass; for (String methodName : new String[]{"get" + StringUtils.capitalize(name), "is" + StringUtils.capitalize(name), name}) { @@ -375,13 +355,21 @@ public class AnnotationAnalyzer extends ModelAnalyzer { String baseClassName = className.substring(0, templateOpenIndex); TypeElement typeElement = getTypeElement(baseClassName, imports); + if (typeElement == null) { + L.e("cannot find type element for %s", baseClassName); + return null; + } ArrayList templateParameters = splitTemplateParameters(paramStr); TypeMirror[] typeArgs = new TypeMirror[templateParameters.size()]; for (int i = 0; i < typeArgs.length; i++) { typeArgs[i] = findClass(templateParameters.get(i), imports).mTypeMirror; + if (typeArgs[i] == null) { + L.e("cannot find type argument for %s in %s", templateParameters.get(i), + baseClassName); + return null; + } } - Types typeUtils = getTypeUtils(); declaredType = typeUtils.getDeclaredType(typeElement, typeArgs); } @@ -457,8 +445,7 @@ public class AnnotationAnalyzer extends ModelAnalyzer { urls.add(resources.nextElement()); } } catch (IOException e) { - printMessage(Diagnostic.Kind.ERROR, "IOException while getting resources: " + - e.getLocalizedMessage()); + L.e(e, "IOException while getting resources:"); } return urls; @@ -470,14 +457,19 @@ public class AnnotationAnalyzer extends ModelAnalyzer { } public Types getTypeUtils() { - return processingEnv.getTypeUtils(); + return mProcessingEnv.getTypeUtils(); } public Elements getElementUtils() { - return processingEnv.getElementUtils(); + return mProcessingEnv.getElementUtils(); } - public void printMessage(Diagnostic.Kind kind, String message) { - processingEnv.getMessager().printMessage(kind, message); + public ProcessingEnvironment getProcessingEnv() { + return mProcessingEnv; + } + + @Override + public TypeUtil createTypeUtil() { + return new AnnotationTypeUtil(this); } } diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationClass.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationClass.java similarity index 88% rename from tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationClass.java rename to tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationClass.java index 00be90f21250c..de54044cc2728 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationClass.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationClass.java @@ -13,9 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.databinding.reflection; +package com.android.databinding.reflection.annotation; -import com.android.databinding.store.SetterStore; +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.reflection.SdkUtil; +import com.android.databinding.reflection.TypeUtil; +import com.android.databinding.util.L; import java.util.ArrayList; import java.util.List; @@ -33,7 +37,7 @@ import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; -public class AnnotationClass implements ModelClass { +class AnnotationClass implements ModelClass { final TypeMirror mTypeMirror; @@ -100,15 +104,13 @@ public class AnnotationClass implements ModelClass { } } if (foundInterface == null) { - printMessage(Diagnostic.Kind.ERROR, - "Detected " + interfaceType + " type for " + mTypeMirror + + L.e("Detected " + interfaceType + " type for " + mTypeMirror + ", but not able to find the implemented interface."); return null; } } if (foundInterface.getKind() != TypeKind.DECLARED) { - printMessage(Diagnostic.Kind.ERROR, - "Found " + interfaceType + " type for " + mTypeMirror + + L.e("Found " + interfaceType + " type for " + mTypeMirror + ", but it isn't a declared type: " + foundInterface); return null; } @@ -285,6 +287,21 @@ public class AnnotationClass implements ModelClass { return null; } + @Override + public String getCanonicalName() { + return getTypeUtils().erasure(mTypeMirror).toString(); + } + + @Override + public int getMinApi() { + return SdkUtil.getMinApi(this); + } + + @Override + public String getJniDescription() { + return TypeUtil.getInstance().getDescription(this); + } + @Override public boolean equals(Object obj) { if (obj instanceof AnnotationClass) { @@ -300,31 +317,27 @@ public class AnnotationClass implements ModelClass { } private static Types getTypeUtils() { - return AnnotationAnalyzer.instance.processingEnv.getTypeUtils(); + return AnnotationAnalyzer.get().mProcessingEnv.getTypeUtils(); } private static Elements getElementUtils() { - return AnnotationAnalyzer.instance.processingEnv.getElementUtils(); + return AnnotationAnalyzer.get().mProcessingEnv.getElementUtils(); } private static AnnotationClass[] getListTypes() { - return AnnotationAnalyzer.instance.getListTypes(); + return AnnotationAnalyzer.get().getListTypes(); } private static AnnotationClass getMapType() { - return AnnotationAnalyzer.instance.getMapType(); + return AnnotationAnalyzer.get().getMapType(); } private static AnnotationClass getStringType() { - return AnnotationAnalyzer.instance.getStringType(); + return AnnotationAnalyzer.get().getStringType(); } private static AnnotationClass getObjectType() { - return AnnotationAnalyzer.instance.getObjectType(); - } - - private static void printMessage(Diagnostic.Kind kind, String message) { - AnnotationAnalyzer.instance.printMessage(kind, message); + return AnnotationAnalyzer.get().getObjectType(); } @Override diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationField.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationField.java similarity index 88% rename from tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationField.java rename to tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationField.java index a2688532624ff..f283366bfd61d 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationField.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationField.java @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.databinding.reflection; +package com.android.databinding.reflection.annotation; + +import com.android.databinding.reflection.ModelField; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; -public class AnnotationField implements ModelField { +class AnnotationField implements ModelField { final VariableElement mField; @@ -38,7 +40,7 @@ public class AnnotationField implements ModelField { public boolean equals(Object obj) { if (obj instanceof AnnotationField) { AnnotationField that = (AnnotationField) obj; - return mDeclaredClass.equals(that.mDeclaredClass) && AnnotationAnalyzer.instance + return mDeclaredClass.equals(that.mDeclaredClass) && AnnotationAnalyzer.get() .getTypeUtils().isSameType(mField.asType(), that.mField.asType()); } else { return false; diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationMethod.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationMethod.java similarity index 70% rename from tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationMethod.java rename to tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationMethod.java index 00967597a2652..33dbab997259c 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/AnnotationMethod.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationMethod.java @@ -13,27 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.databinding.reflection; +package com.android.databinding.reflection.annotation; + +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.reflection.SdkUtil; +import com.android.databinding.reflection.TypeUtil; import java.util.List; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.ExecutableType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; -public class AnnotationMethod implements ModelMethod { +class AnnotationMethod implements ModelMethod { final ExecutableType mMethod; final DeclaredType mDeclaringType; final ExecutableElement mExecutableElement; + int mApiLevel = -1; // calculated on demand public AnnotationMethod(DeclaredType declaringType, ExecutableElement executableElement) { mDeclaringType = declaringType; mExecutableElement = executableElement; - Types typeUtils = AnnotationAnalyzer.instance.getTypeUtils(); + Types typeUtils = AnnotationAnalyzer.get().getTypeUtils(); mMethod = (ExecutableType) typeUtils.asMemberOf(declaringType, executableElement); } @@ -74,4 +79,27 @@ public class AnnotationMethod implements ModelMethod { public boolean isStatic() { return mExecutableElement.getModifiers().contains(Modifier.STATIC); } + + @Override + public int getMinApi() { + if (mApiLevel == -1) { + mApiLevel = SdkUtil.getMinApi(this); + } + return mApiLevel; + } + + @Override + public String getJniDescription() { + return TypeUtil.getInstance().getDescription(this); + } + + @Override + public String toString() { + return "AnnotationMethod{" + + "mMethod=" + mMethod + + ", mDeclaringType=" + mDeclaringType + + ", mExecutableElement=" + mExecutableElement + + ", mApiLevel=" + mApiLevel + + '}'; + } } diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationTypeUtil.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationTypeUtil.java new file mode 100644 index 0000000000000..03f93dc369285 --- /dev/null +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/reflection/annotation/AnnotationTypeUtil.java @@ -0,0 +1,105 @@ +/* + * 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 com.android.databinding.reflection.annotation; + +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.reflection.TypeUtil; + +import java.util.List; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +public class AnnotationTypeUtil extends TypeUtil { + javax.lang.model.util.Types mTypes; + + public AnnotationTypeUtil( + AnnotationAnalyzer annotationAnalyzer) { + mTypes = annotationAnalyzer.getTypeUtils(); + } + + @Override + public String getDescription(ModelClass modelClass) { + // TODO use interface + return modelClass.getCanonicalName().replace('.', '/'); + } + + @Override + public String getDescription(ModelMethod modelMethod) { + // TODO use interface + return modelMethod.getName() + getDescription( + ((AnnotationMethod) modelMethod).mExecutableElement.asType()); + } + + private String getDescription(TypeMirror typeMirror) { + if (typeMirror == null) { + throw new UnsupportedOperationException(); + } + switch (typeMirror.getKind()) { + case BOOLEAN: + return BOOLEAN; + case BYTE: + return BYTE; + case SHORT: + return SHORT; + case INT: + return INT; + case LONG: + return LONG; + case CHAR: + return CHAR; + case FLOAT: + return FLOAT; + case DOUBLE: + return DOUBLE; + case DECLARED: + return CLASS_PREFIX + mTypes.erasure(typeMirror).toString().replace('.', '/') + CLASS_SUFFIX; + case VOID: + return VOID; + case ARRAY: + final ArrayType arrayType = (ArrayType) typeMirror; + final String componentType = getDescription(arrayType.getComponentType()); + return ARRAY + componentType; + case TYPEVAR: + final TypeVariable typeVariable = (TypeVariable) typeMirror; + final String name = typeVariable.toString(); + return CLASS_PREFIX + name.replace('.', '/') + CLASS_SUFFIX; + case EXECUTABLE: + final ExecutableType executableType = (ExecutableType) typeMirror; + final int argStart = mTypes.erasure(executableType).toString().indexOf('('); + final String methodName = executableType.toString().substring(0, argStart); + final String args = joinArgs(executableType.getParameterTypes()); + // TODO detect constructor? + return methodName + "(" + args + ")" + getDescription( + executableType.getReturnType()); + default: + throw new UnsupportedOperationException("cannot understand type " + + typeMirror.toString() + ", kind:" + typeMirror.getKind().name()); + } + } + + private String joinArgs(List mirrorList) { + StringBuilder result = new StringBuilder(); + for (TypeMirror mirror : mirrorList) { + result.append(getDescription(mirror)); + } + return result.toString(); + } +} diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/store/SetterStore.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/store/SetterStore.java index 008f2da537465..77e1e341fcb1f 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/store/SetterStore.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/store/SetterStore.java @@ -15,10 +15,12 @@ */ package com.android.databinding.store; -import com.android.databinding.reflection.AnnotationAnalyzer; import com.android.databinding.reflection.ModelAnalyzer; import com.android.databinding.reflection.ModelClass; import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.util.L; + +import org.apache.commons.lang3.StringUtils; import java.io.File; import java.io.IOException; @@ -43,7 +45,6 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; -import javax.tools.Diagnostic; import javax.tools.FileObject; import javax.tools.StandardLocation; @@ -111,13 +112,9 @@ public class SetterStore { } return new SetterStore(modelAnalyzer, store); } catch (IOException e) { - printMessage(Diagnostic.Kind.ERROR, "Could not read SetterStore intermediate file: " + - e.getLocalizedMessage()); - e.printStackTrace(); + L.e(e, "Could not read SetterStore intermediate file"); } catch (ClassNotFoundException e) { - printMessage(Diagnostic.Kind.ERROR, "Could not read SetterStore intermediate file: " + - e.getLocalizedMessage()); - e.printStackTrace(); + L.e(e, "Could not read SetterStore intermediate file"); } return new SetterStore(modelAnalyzer, store); } @@ -255,8 +252,7 @@ public class SetterStore { Filer filer = processingEnvironment.getFiler(); FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, SetterStore.class.getPackage().getName(), "setter_store.bin"); - printMessage(Diagnostic.Kind.NOTE, "============= Writing intermediate file: " + - resource.getName()); + L.d("============= Writing intermediate file: %s", resource.getName()); ObjectOutputStream out = null; try { out = new ObjectOutputStream(resource.openOutputStream()); @@ -269,17 +265,17 @@ public class SetterStore { } } - public String getSetterCall(String attribute, ModelClass viewType, - ModelClass valueType, String viewExpression, String valueExpression, - Map imports) { + public SetterCall getSetterCall(String attribute, ModelClass viewType, + ModelClass valueType, Map imports) { if (!attribute.startsWith("android:")) { int colon = attribute.indexOf(':'); if (colon >= 0) { attribute = attribute.substring(colon + 1); } } + SetterCall setterCall = null; MethodDescription adapter = null; - String setterName = null; + MethodDescription conversionMethod = null; if (viewType != null) { HashMap adapters = mStore.adapterMethods.get(attribute); ModelMethod bestSetterMethod = getBestSetter(viewType, valueType, attribute, imports); @@ -288,7 +284,7 @@ public class SetterStore { if (bestSetterMethod != null) { bestViewType = bestSetterMethod.getDeclaringClass(); bestValueType = bestSetterMethod.getParameterTypes()[0]; - setterName = bestSetterMethod.getName(); + setterCall = new ModelMethodSetter(bestSetterMethod); } if (adapters != null) { @@ -299,8 +295,7 @@ public class SetterStore { if (adapterViewType.isAssignableFrom(viewType)) { try { ModelClass adapterValueType = mClassAnalyzer - .findClass(key.valueType, - imports); + .findClass(key.valueType, imports); boolean isBetterView = bestViewType == null || bestValueType.isAssignableFrom(adapterValueType); if (isBetterParameter(valueType, adapterValueType, bestValueType, @@ -308,35 +303,29 @@ public class SetterStore { bestViewType = adapterViewType; bestValueType = adapterValueType; adapter = adapters.get(key); + setterCall = new AdapterSetter(adapter); } } catch (Exception e) { - printMessage(Diagnostic.Kind.NOTE, - "Unknown class: " + key.valueType); + L.e(e, "Unknown class: %s", key.valueType); } } } catch (Exception e) { - printMessage(Diagnostic.Kind.NOTE, "Unknown class: " + key.viewType); + L.e(e, "Unknown class: %s", key.viewType); } } } - MethodDescription conversionMethod = getConversionMethod(valueType, bestValueType, - imports); - if (conversionMethod != null) { - valueExpression = conversionMethod.type + "." + conversionMethod.method + "(" + - valueExpression + ")"; - } + conversionMethod = getConversionMethod(valueType, bestValueType, imports); } - if (adapter == null) { - if (setterName == null) { - setterName = getDefaultSetter(attribute); - } - return viewExpression + "." + setterName + "(" + valueExpression + ")"; - } else { - return adapter.type + "." + adapter.method + "(" + viewExpression + ", " + - valueExpression + ")"; + if (setterCall == null) { + setterCall = new DummySetter(getDefaultSetter(attribute)); + // might be an include tag etc. just note it and continue. + L.d("Cannot find the setter for attribute " + attribute + ". might be an include file," + + " moving on."); } + setterCall.setConverter(conversionMethod); + return setterCall; } public boolean isUntaggable(String viewType) { @@ -345,15 +334,14 @@ public class SetterStore { private ModelMethod getBestSetter(ModelClass viewType, ModelClass argumentType, String attribute, Map imports) { - String setterName = null; - + List setterCandidates = new ArrayList<>(); HashMap renamed = mStore.renamedMethods.get(attribute); if (renamed != null) { for (String className : renamed.keySet()) { try { ModelClass renamedViewType = mClassAnalyzer.findClass(className, imports); if (renamedViewType.isAssignableFrom(viewType)) { - setterName = renamed.get(className).method; + setterCandidates.add(renamed.get(className).method); break; } } catch (Exception e) { @@ -361,34 +349,40 @@ public class SetterStore { } } } - if (setterName == null) { - setterName = getDefaultSetter(attribute); - } - ModelMethod[] methods = viewType.getMethods(setterName, 1); + setterCandidates.add(getDefaultSetter(attribute)); + setterCandidates.add(trimAttributeNamespace(attribute)); - ModelClass bestParameterType = null; ModelMethod bestMethod = null; - List args = new ArrayList<>(); - args.add(argumentType); - for (ModelMethod method : methods) { - ModelClass[] parameterTypes = method.getParameterTypes(); - if (method.getReturnType(args).isVoid() && !method.isStatic() && method.isPublic()) { - ModelClass param = parameterTypes[0]; - if (isBetterParameter(argumentType, param, bestParameterType, true, imports)) { - bestParameterType = param; - bestMethod = method; + for (String name : setterCandidates) { + ModelMethod[] methods = viewType.getMethods(name, 1); + ModelClass bestParameterType = null; + + List args = new ArrayList<>(); + args.add(argumentType); + for (ModelMethod method : methods) { + ModelClass[] parameterTypes = method.getParameterTypes(); + if (method.getReturnType(args).isVoid() && !method.isStatic() && method + .isPublic()) { + ModelClass param = parameterTypes[0]; + if (isBetterParameter(argumentType, param, bestParameterType, true, imports)) { + bestParameterType = param; + bestMethod = method; + + } } } } return bestMethod; + + } + + private static String trimAttributeNamespace(String attribute) { + final int colonIndex = attribute.indexOf(':'); + return colonIndex == -1 ? attribute : attribute.substring(colonIndex + 1); } private static String getDefaultSetter(String attribute) { - int colonIndex = attribute.indexOf(':'); - String propertyName; - propertyName = Character.toUpperCase(attribute.charAt(colonIndex + 1)) + - attribute.substring(colonIndex + 2); - return "set" + propertyName; + return "set" + StringUtils.capitalize(trimAttributeNamespace(attribute)); } private boolean isBetterParameter(ModelClass argument, ModelClass parameter, @@ -463,12 +457,12 @@ public class SetterStore { return conversion.get(toClassName); } } catch (Exception e) { - printMessage(Diagnostic.Kind.NOTE, "Unknown class: " + toClassName); + L.d(e, "Unknown class: %s", toClassName); } } } } catch (Exception e) { - printMessage(Diagnostic.Kind.NOTE, "Unknown class: " + fromClassName); + L.d(e, "Unknown class: %s", fromClassName); } } } @@ -549,15 +543,6 @@ public class SetterStore { } } - private static void printMessage(Diagnostic.Kind kind, String message) { - ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance(); - if (modelAnalyzer instanceof AnnotationAnalyzer) { - ((AnnotationAnalyzer) modelAnalyzer).printMessage(kind, message); - } else { - System.out.println(message); - } - } - private static class MethodDescription implements Serializable { private static final long serialVersionUID = 1; @@ -569,12 +554,14 @@ public class SetterStore { public MethodDescription(String type, String method) { this.type = type; this.method = method; + L.d("BINARY created method desc 1 %s %s", type, method ); } public MethodDescription(ExecutableElement method) { TypeElement enclosingClass = (TypeElement) method.getEnclosingElement(); this.type = enclosingClass.getQualifiedName().toString(); this.method = method.getSimpleName().toString(); + L.d("BINARY created method desc 2 %s %s, %s", type, this.method, method); } @Override @@ -654,4 +641,85 @@ public class SetterStore { return this; } } + + public static class DummySetter extends SetterCall { + private String mMethodName; + + public DummySetter(String methodName) { + mMethodName = methodName; + } + + @Override + public String toJavaInternal(String viewExpression, String valueExpression) { + return viewExpression + "." + mMethodName + "(" + valueExpression + ")"; + } + + @Override + public int getMinApi() { + return 1; + } + + + } + + public static class AdapterSetter extends SetterCall { + final MethodDescription mAdapter; + + public AdapterSetter(MethodDescription adapter) { + mAdapter = adapter; + } + + @Override + public String toJavaInternal(String viewExpression, String valueExpression) { + return mAdapter.type + "." + mAdapter.method + "(" + viewExpression + ", " + + valueExpression + ")"; + } + + @Override + public int getMinApi() { + return 1; + } + } + + public static class ModelMethodSetter extends SetterCall { + final ModelMethod mModelMethod; + + public ModelMethodSetter(ModelMethod modelMethod) { + mModelMethod = modelMethod; + } + + @Override + public String toJavaInternal(String viewExpression, String valueExpression) { + return viewExpression + "." + mModelMethod.getName() + "(" + valueExpression + ")"; + } + + @Override + public int getMinApi() { + return mModelMethod.getMinApi(); + } + } + + public static abstract class SetterCall { + private MethodDescription mConverter; + + public SetterCall() { + } + + public void setConverter(MethodDescription converter) { + mConverter = converter; + } + + protected abstract String toJavaInternal(String viewExpression, String converted); + + public final String toJava(String viewExpression, String valueExpression) { + return toJavaInternal(viewExpression, convertValue(valueExpression)); + } + + protected String convertValue(String valueExpression) { + return mConverter == null ? valueExpression : + mConverter.type + "." + mConverter.method + "(" + valueExpression + ")"; + } + + abstract public int getMinApi(); + } } diff --git a/tools/data-binding/compiler/src/main/java/com/android/databinding/util/L.java b/tools/data-binding/compiler/src/main/java/com/android/databinding/util/L.java index c59e3a70f0c70..eea89a2c95d6b 100644 --- a/tools/data-binding/compiler/src/main/java/com/android/databinding/util/L.java +++ b/tools/data-binding/compiler/src/main/java/com/android/databinding/util/L.java @@ -16,19 +16,48 @@ package com.android.databinding.util; +import com.android.databinding.reflection.annotation.AnnotationAnalyzer; +import com.android.databinding.reflection.ModelAnalyzer; + import org.apache.commons.lang3.exception.ExceptionUtils; +import javax.tools.Diagnostic; + public class L { public static void d(String msg, Object... args) { - System.out.println("[LDEBUG] " + String.format(msg, args)); + printMessage(Diagnostic.Kind.NOTE, String.format(msg, args)); + } + + public static void d(Throwable t, String msg, Object... args) { + printMessage(Diagnostic.Kind.NOTE, + String.format(msg, args) + " " + ExceptionUtils.getStackTrace(t)); } public static void e(String msg, Object... args) { - System.out.println("[LERROR] " + String.format(msg, args)); + printMessage(Diagnostic.Kind.ERROR, String.format(msg, args)); } + public static void e(Throwable t, String msg, Object... args) { - System.out - .println("[LERROR]" + String.format(msg, args) + " " + ExceptionUtils.getStackTrace(t)); + printMessage(Diagnostic.Kind.ERROR, + String.format(msg, args) + " " + ExceptionUtils.getStackTrace(t)); } + + private static void printMessage(Diagnostic.Kind kind, String message) { + ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance(); + System.out.println("[" + kind.name() + "]: " + message); + if (modelAnalyzer instanceof AnnotationAnalyzer) { + ((AnnotationAnalyzer) modelAnalyzer).getProcessingEnv().getMessager() + .printMessage(kind, message); + if (kind == Diagnostic.Kind.ERROR) { + throw new RuntimeException("failure, see logs for details.\n" + message); + } + } else { + + if (kind == Diagnostic.Kind.ERROR) { + throw new RuntimeException(message); + } + } + } + } diff --git a/tools/data-binding/compiler/src/main/kotlin/com/android/databinding/writer/LayoutBinderWriter.kt b/tools/data-binding/compiler/src/main/kotlin/com/android/databinding/writer/LayoutBinderWriter.kt index d62abe96f252e..b4b5986b0d09d 100644 --- a/tools/data-binding/compiler/src/main/kotlin/com/android/databinding/writer/LayoutBinderWriter.kt +++ b/tools/data-binding/compiler/src/main/kotlin/com/android/databinding/writer/LayoutBinderWriter.kt @@ -620,7 +620,16 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) { }.joinToString(" || ") }) {") { it.value.forEach { binding -> - tab("${binding.toJavaCode(binding.getTarget().fieldName, binding.getExpr().toCode().generate())};") + tab("// api target ${binding.getMinApi()}") + val bindingCode = binding.toJavaCode(binding.getTarget().fieldName, binding.getExpr().toCode().generate()) + if (binding.getMinApi() > 1) { + tab("if(com.android.databinding.library.DataBinder.getBuildSdkInt() >= ${binding.getMinApi()}) {") { + tab("$bindingCode;") + } + tab("}") + } else { + tab("$bindingCode;") + } } } tab("}") diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/ExpressionVisitorTest.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/ExpressionVisitorTest.java index 8237aee8c0cc3..abf8f9aadbc4c 100644 --- a/tools/data-binding/compiler/src/test/java/com/android/databinding/ExpressionVisitorTest.java +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/ExpressionVisitorTest.java @@ -27,6 +27,8 @@ import com.android.databinding.expr.SymbolExpr; import com.android.databinding.expr.TernaryExpr; import com.android.databinding.reflection.Callable; import com.android.databinding.reflection.ModelAnalyzer; +import com.android.databinding.reflection.java.JavaAnalyzer; +import com.android.databinding.reflection.java.JavaClass; import org.junit.Before; import org.junit.Test; @@ -43,7 +45,7 @@ public class ExpressionVisitorTest { @Before public void setUp() throws Exception { - ModelAnalyzer.initForTests(); + JavaAnalyzer.initForTests(); } private T parse(String input, Class klass) { @@ -57,7 +59,7 @@ public class ExpressionVisitorTest { final SymbolExpr res = parse("null", SymbolExpr.class); assertEquals(1, mParser.getModel().size()); assertEquals("null", res.getText()); - assertSame(res.getResolvedType(), Object.class); + assertEquals(new JavaClass(Object.class),res.getResolvedType()); assertEquals(0, res.getDependencies().size()); } @@ -69,7 +71,7 @@ public class ExpressionVisitorTest { @Before public void setUp() throws Exception { - ModelAnalyzer.initForTests(); + JavaAnalyzer.initForTests(); } @Parameterized.Parameters @@ -115,7 +117,7 @@ public class ExpressionVisitorTest { assertEquals(1, mParser.mModel.size()); assertEquals("myStr", id.getName()); id.setUserDefinedType("java.lang.String"); - assertSame(String.class, id.getResolvedType()); + assertEquals(new JavaClass(String.class), id.getResolvedType()); } @Test @@ -142,7 +144,7 @@ public class ExpressionVisitorTest { assertTrue(parsed.getChild() instanceof IdentifierExpr); final IdentifierExpr id = (IdentifierExpr) parsed.getChild(); id.setUserDefinedType("java.lang.String"); - assertSame(int.class, parsed.getResolvedType()); + assertEquals(new JavaClass(int.class), parsed.getResolvedType()); Callable getter = parsed.getGetter(); assertEquals(Callable.Type.METHOD, getter.type); assertEquals("length", getter.name); @@ -158,7 +160,7 @@ public class ExpressionVisitorTest { assertTrue(parsed.getChild() instanceof IdentifierExpr); final IdentifierExpr id = (IdentifierExpr) parsed.getChild(); id.setUserDefinedType("java.lang.String"); - assertSame(byte[].class, parsed.getResolvedType()); + assertEquals(new JavaClass(byte[].class), parsed.getResolvedType()); Callable getter = parsed.getGetter(); assertEquals(Callable.Type.METHOD, getter.type); assertEquals("getBytes", getter.name); diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/LayoutBinderTest.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/LayoutBinderTest.java index 4342f03d9c6d2..8dfda2f86ee7b 100644 --- a/tools/data-binding/compiler/src/test/java/com/android/databinding/LayoutBinderTest.java +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/LayoutBinderTest.java @@ -21,6 +21,7 @@ import com.android.databinding.expr.IdentifierExpr; import com.android.databinding.expr.StaticIdentifierExpr; import com.android.databinding.reflection.Callable; import com.android.databinding.reflection.ModelAnalyzer; +import com.android.databinding.reflection.java.JavaClass; import org.junit.Before; import org.junit.Test; @@ -35,8 +36,7 @@ public class LayoutBinderTest { ExprModel mExprModel; @Before public void setUp() throws Exception { - ModelAnalyzer.initForTests(); - mLayoutBinder = new LayoutBinder(null); + mLayoutBinder = new MockLayoutBinder(); mExprModel = mLayoutBinder.getModel(); } @@ -49,7 +49,7 @@ public class LayoutBinderTest { assertEquals(value.getClass(), IdentifierExpr.class); final IdentifierExpr id = (IdentifierExpr) value; assertEquals("test", id.getName()); - assertEquals(String.class, id.getResolvedType()); + assertEquals(new JavaClass(String.class), id.getResolvedType()); assertTrue(id.isDynamic()); } @@ -62,7 +62,7 @@ public class LayoutBinderTest { assertEquals(value.getClass(), StaticIdentifierExpr.class); final IdentifierExpr id = (IdentifierExpr) value; assertEquals("test", id.getName()); - assertEquals(String.class, id.getResolvedType()); + assertEquals(new JavaClass(String.class), id.getResolvedType()); assertFalse(id.isDynamic()); } diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockIViewDataBinder.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/MockIViewDataBinder.java deleted file mode 100644 index db8404c703374..0000000000000 --- a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockIViewDataBinder.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 com.android.databinding; - -public interface MockIViewDataBinder { - -} diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservableLsit.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/MockLayoutBinder.java similarity index 71% rename from tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservableLsit.java rename to tools/data-binding/compiler/src/test/java/com/android/databinding/MockLayoutBinder.java index 41c8abcdc8938..dcdad423c1815 100644 --- a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservableLsit.java +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/MockLayoutBinder.java @@ -13,6 +13,12 @@ package com.android.databinding; -public class MockObservableLsit { +import com.android.databinding.store.ResourceBundle; +public class MockLayoutBinder extends LayoutBinder { + + public MockLayoutBinder() { + super(new ResourceBundle("com.test"), + new ResourceBundle.LayoutFileBundle("blah.xml", 1, ".")); + } } diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservableMap.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservableMap.java deleted file mode 100644 index 1d7add89e382b..0000000000000 --- a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockObservableMap.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 com.android.databinding; - -public class MockObservableMap { - -} diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/SdkVersionTest.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/SdkVersionTest.java new file mode 100644 index 0000000000000..2c85f69ad1f8a --- /dev/null +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/SdkVersionTest.java @@ -0,0 +1,49 @@ +/* + * 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 com.android.databinding; + +import com.android.databinding.expr.ExprModel; +import com.android.databinding.reflection.ModelAnalyzer; +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.reflection.SdkUtil; +import com.android.databinding.reflection.java.JavaAnalyzer; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; + +public class SdkVersionTest { + + @Before + public void setUp() throws Exception { + JavaAnalyzer.initForTests(); + } + + @Test + public void testNewApiMethod() { + ModelClass view = ModelAnalyzer.getInstance().findClass("android.view.View", null); + ModelMethod setElevation = view.getMethods("setElevation", 1)[0]; + assertEquals(21, SdkUtil.getMinApi(setElevation)); + } + + @Test + public void testCustomCode() { + ModelClass view = ModelAnalyzer.getInstance() + .findClass("com.android.databinding.SdkVersionTest", null); + ModelMethod setElevation = view.getMethods("testCustomCode", 0)[0]; + assertEquals(1, SdkUtil.getMinApi(setElevation)); + } +} diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/expr/ExprModelTest.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/expr/ExprModelTest.java index 5fb3802643e65..9dded3ae90b37 100644 --- a/tools/data-binding/compiler/src/test/java/com/android/databinding/expr/ExprModelTest.java +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/expr/ExprModelTest.java @@ -19,8 +19,12 @@ package com.android.databinding.expr; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import com.android.databinding.MockLayoutBinder; import com.android.databinding.reflection.ModelAnalyzer; import com.android.databinding.LayoutBinder; +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.java.JavaAnalyzer; +import com.android.databinding.store.ResourceBundle; import com.android.databinding.util.L; import org.apache.commons.lang3.ArrayUtils; @@ -31,6 +35,7 @@ import org.junit.Test; import org.junit.rules.TestWatcher; import org.junit.runner.Description; +import java.io.File; import java.util.BitSet; import java.util.List; @@ -45,8 +50,8 @@ public class ExprModelTest { } @Override - protected Class resolveType(ModelAnalyzer modelAnalyzer) { - return Integer.class; + protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) { + return modelAnalyzer.findClass(Integer.class); } @Override @@ -77,7 +82,7 @@ public class ExprModelTest { @Before public void setUp() throws Exception { - ModelAnalyzer.initForTests(); + JavaAnalyzer.initForTests(); mExprModel = new ExprModel(); } @@ -118,7 +123,7 @@ public class ExprModelTest { @Test public void testShouldRead() { - LayoutBinder lb = new LayoutBinder(null); + LayoutBinder lb = new MockLayoutBinder(); mExprModel = lb.getModel(); IdentifierExpr a = lb.addVariable("a", "java.lang.String"); IdentifierExpr b = lb.addVariable("b", "java.lang.String"); @@ -140,7 +145,7 @@ public class ExprModelTest { @Test public void testTernaryInsideTernary() { - LayoutBinder lb = new LayoutBinder(null); + LayoutBinder lb = new MockLayoutBinder(); mExprModel = lb.getModel(); IdentifierExpr cond1 = lb.addVariable("cond1", "boolean"); IdentifierExpr cond2 = lb.addVariable("cond2", "boolean"); @@ -185,7 +190,7 @@ public class ExprModelTest { @Test public void testRequirementFlags() { - LayoutBinder lb = new LayoutBinder(null); + LayoutBinder lb = new MockLayoutBinder(); mExprModel = lb.getModel(); IdentifierExpr a = lb.addVariable("a", "java.lang.String"); IdentifierExpr b = lb.addVariable("b", "java.lang.String"); @@ -256,7 +261,7 @@ public class ExprModelTest { @Test public void testPostConditionalDependencies() { - LayoutBinder lb = new LayoutBinder(null); + LayoutBinder lb = new MockLayoutBinder(); mExprModel = lb.getModel(); @@ -273,7 +278,7 @@ public class ExprModelTest { Expr bcCmp = bcTernary.getPred(); Expr u1GetCondD = ((TernaryExpr) bcTernary.getIfTrue()).getPred(); final MathExpr xxPlusU2getCondE = (MathExpr) bcTernary.getIfFalse(); - Expr u2GetCondE = xxPlusU2getCondE.mRight; + Expr u2GetCondE = xxPlusU2getCondE.getRight(); Expr u1Name = abTernary.getIfTrue(); Expr u2Name = abTernary.getIfFalse(); Expr u1LastName = ((TernaryExpr) bcTernary.getIfTrue()).getIfTrue(); @@ -338,7 +343,7 @@ public class ExprModelTest { @Test public void testCircularDependency() { - LayoutBinder lb = new LayoutBinder(null); + LayoutBinder lb = new MockLayoutBinder(); mExprModel = lb.getModel(); IdentifierExpr a = lb.addVariable("a", int.class.getCanonicalName()); IdentifierExpr b = lb.addVariable("b", int.class.getCanonicalName()); @@ -354,7 +359,7 @@ public class ExprModelTest { @Test public void testNestedCircularDependency() { - LayoutBinder lb = new LayoutBinder(null); + LayoutBinder lb = new MockLayoutBinder(); mExprModel = lb.getModel(); IdentifierExpr a = lb.addVariable("a", int.class.getCanonicalName()); IdentifierExpr b = lb.addVariable("b", int.class.getCanonicalName()); @@ -374,7 +379,7 @@ public class ExprModelTest { @Test public void testNoFlagsForNonBindingStatic() { - LayoutBinder lb = new LayoutBinder(null); + LayoutBinder lb = new MockLayoutBinder(); mExprModel = lb.getModel(); lb.addVariable("a", int.class.getCanonicalName()); final MathExpr parsed = parse(lb, "a * (3 + 2)", MathExpr.class); @@ -386,7 +391,7 @@ public class ExprModelTest { @Test public void testFlagsForBindingStatic() { - LayoutBinder lb = new LayoutBinder(null); + LayoutBinder lb = new MockLayoutBinder(); mExprModel = lb.getModel(); lb.addVariable("a", int.class.getCanonicalName()); final Expr staticParsed = parse(lb, "3 + 2", MathExpr.class); @@ -401,7 +406,6 @@ public class ExprModelTest { @Test public void testPartialNeededRead() { - throw new NotImplementedException("create a test that has a variable which can be read for " + "some flags and also may be read for some condition. Try both must match and" + " partial match and none-match in conditionals"); diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/expr/ExprTest.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/expr/ExprTest.java index 32a8ef40d17a6..a3618e3f305d7 100644 --- a/tools/data-binding/compiler/src/test/java/com/android/databinding/expr/ExprTest.java +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/expr/ExprTest.java @@ -17,6 +17,8 @@ package com.android.databinding.expr; import com.android.databinding.reflection.ModelAnalyzer; +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.java.JavaAnalyzer; import org.junit.Before; import org.junit.Test; @@ -37,8 +39,8 @@ public class ExprTest{ } @Override - protected Class resolveType(ModelAnalyzer modelAnalyzer) { - return Integer.class; + protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) { + return modelAnalyzer.findClass(Integer.class); } @Override @@ -59,15 +61,15 @@ public class ExprTest{ @Before public void setUp() throws Exception { - ModelAnalyzer.initForTests(); + JavaAnalyzer.initForTests(); } @Test(expected=IllegalStateException.class) public void testBadExpr() { Expr expr = new Expr() { @Override - protected Class resolveType(ModelAnalyzer modelAnalyzer) { - return Integer.class; + protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) { + return modelAnalyzer.findClass(Integer.class); } @Override diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaAnalyzer.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaAnalyzer.java new file mode 100644 index 0000000000000..f03d77c4e5063 --- /dev/null +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaAnalyzer.java @@ -0,0 +1,361 @@ +/* + * 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 com.android.databinding.reflection.java; + +import com.google.common.collect.ImmutableMap; +import com.android.databinding.reflection.Callable; +import com.android.databinding.reflection.ModelAnalyzer; +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.ModelField; +import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.reflection.SdkUtil; +import com.android.databinding.reflection.TypeUtil; +import com.android.databinding.util.L; + +import org.apache.commons.lang3.StringUtils; + +import android.binding.Bindable; +import android.binding.Observable; +import android.binding.ObservableList; +import android.binding.ObservableMap; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JavaAnalyzer extends ModelAnalyzer { + public static final Map PRIMITIVE_TYPES = + new ImmutableMap.Builder() + .put("boolean", boolean.class) + .put("byte", byte.class) + .put("short", short.class) + .put("char", char.class) + .put("int", int.class) + .put("long", long.class) + .put("float", float.class) + .put("double", double.class) + .build(); + private static final String BINDABLE_ANNOTATION_NAME = "android.binding.Bindable"; + + private HashMap mClassCache = new HashMap<>(); + + private final ClassLoader mClassLoader; + + private final Class mObservable; + + private final Class mObservableList; + + private final Class mObservableMap; + + private final Class[] mObservableFields; + + private final Class mBindable; + + private final boolean mTestMode; + + private final Class mIViewDataBinder; + + public JavaAnalyzer(ClassLoader classLoader, boolean testMode) { + setInstance(this); + mClassLoader = classLoader; + mTestMode = testMode; + try { + mIViewDataBinder = classLoader.loadClass(I_VIEW_DATA_BINDER); + mObservable = Observable.class; + mObservableList = ObservableList.class; + mObservableMap = ObservableMap.class; + mBindable = Bindable.class; + mObservableFields = new Class[OBSERVABLE_FIELDS.length]; + for (int i = 0; i < OBSERVABLE_FIELDS.length; i++) { + mObservableFields[i] = classLoader.loadClass(getClassName(OBSERVABLE_FIELDS[i])); + } + + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private String getClassName(String name) { + return name; + } + + @Override + public boolean isDataBinder(ModelClass reflectionClass) { + JavaClass javaClass = (JavaClass) reflectionClass; + return mIViewDataBinder.isAssignableFrom(javaClass.mClass); + } + + @Override + public Callable findMethod(ModelClass modelClass, String name, List argClasses, + boolean staticAccess) { + Class klass = ((JavaClass) modelClass).mClass; + ArrayList args = new ArrayList<>(argClasses.size()); + for (int i = 0; i < argClasses.size(); i++) { + args.add(((JavaClass) argClasses.get(i)).mClass); + } + // TODO implement properly + for (String methodName : new String[]{"set" + StringUtils.capitalize(name), name}) { + for (Method method : klass.getMethods()) { + if (methodName.equals(method.getName()) && args.size() == method + .getParameterTypes().length) { + return new Callable(Callable.Type.METHOD, methodName, + new JavaClass(method.getReturnType()), true, false); + } + } + } + L.e(new Exception(), "cannot find method %s in %s", name, klass); + throw new IllegalArgumentException( + "cannot find method " + name + " at class " + klass.getSimpleName()); + } + + @Override + public boolean isObservable(ModelClass modelClass) { + Class klass = ((JavaClass) modelClass).mClass; + return isObservable(klass); + } + + private boolean isObservable(Class klass) { + return mObservable.isAssignableFrom(klass) || mObservableList.isAssignableFrom(klass) || + mObservableMap.isAssignableFrom(klass); + } + + @Override + public boolean isObservableField(ModelClass reflectionClass) { + Class klass = ((JavaClass) reflectionClass).mClass; + for (Class observableField : mObservableFields) { + if (observableField.isAssignableFrom(klass)) { + return true; + } + } + return false; + } + + @Override + public boolean isBindable(ModelField reflectionField) { + Field field = ((JavaField) reflectionField).mField; + return isBindable(field); + } + + @Override + public boolean isBindable(ModelMethod reflectionMethod) { + Method method = ((JavaMethod) reflectionMethod).mMethod; + return isBindable(method); + } + + private boolean isBindable(Field field) { + return field.getAnnotation(mBindable) != null; + } + + private boolean isBindable(Method method) { + return method.getAnnotation(mBindable) != null; + } + + @Override + public Callable findMethodOrField(ModelClass modelClass, String name, boolean staticAccess) { + final Class klass = ((JavaClass) modelClass).mClass; + for (String methodName : + new String[]{"get" + StringUtils.capitalize(name), + "is" + StringUtils.capitalize(name), name}) { + try { + Method method = klass.getMethod(methodName); + Field backingField = findField(klass, name, true); + if (Modifier.isPublic(method.getModifiers())) { + final Callable result = new Callable(Callable.Type.METHOD, methodName, + new JavaClass(method.getReturnType()), true, + isBindable(method) || (backingField != null && isBindable(backingField)) ); + L.d("backing field for %s is %s", result, backingField); + return result; + } + } catch (Throwable t) { + + } + } + try { + Field field = findField(klass, name, false); + if (Modifier.isPublic(field.getModifiers())) { + return new Callable(Callable.Type.FIELD, name, new JavaClass(field.getType()), + !Modifier.isFinal(field.getModifiers()) + || isObservable(field.getType()), isBindable(field)); + } + } catch (Throwable t) { + + } + throw new IllegalArgumentException( + "cannot find " + name + " in " + klass.getCanonicalName()); + } + + private Field findField(Class klass, String name, boolean allowNonPublic) { + try { + return getField(klass, name, allowNonPublic); + } catch (NoSuchFieldException e) { + + } + String capitalizedName = StringUtils.capitalize(name); + + try { + return getField(klass, "m" + capitalizedName, allowNonPublic); + } catch (Throwable t){} + try { + return getField(klass, "_" + name, allowNonPublic); + } catch (Throwable t){} + try { + return getField(klass, "_" + capitalizedName, allowNonPublic); + } catch (Throwable t){} + try { + return getField(klass, "m_" + name, allowNonPublic); + } catch (Throwable t){} + try { + return getField(klass, "m_" + capitalizedName, allowNonPublic); + } catch (Throwable t){} + return null; + } + + private Field getField(Class klass, String exactName, boolean allowNonPublic) + throws NoSuchFieldException { + try { + return klass.getField(exactName); + } catch (NoSuchFieldException e) { + if (allowNonPublic) { + return klass.getDeclaredField(exactName); + } else { + throw e; + } + } + } + + @Override + public JavaClass loadPrimitive(String className) { + Class clazz = PRIMITIVE_TYPES.get(className); + if (clazz == null) { + return null; + } else { + return new JavaClass(clazz); + } + } + + @Override + public ModelClass findClass(String className, Map imports) { + // TODO handle imports + JavaClass loaded = mClassCache.get(className); + if (loaded != null) { + return loaded; + } + L.d("trying to load class %s from %s", className, mClassLoader.toString()); + loaded = loadPrimitive(className); + if (loaded == null) { + try { + if (className.startsWith("[") && className.contains("L")) { + int indexOfL = className.indexOf('L'); + JavaClass baseClass = (JavaClass) findClass( + className.substring(indexOfL + 1, className.length() - 1), null); + String realClassName = className.substring(0, indexOfL + 1) + + baseClass.mClass.getCanonicalName() + ';'; + loaded = new JavaClass(Class.forName(realClassName, false, mClassLoader)); + mClassCache.put(className, loaded); + } else { + loaded = loadRecursively(className); + mClassCache.put(className, loaded); + } + + } catch (Throwable t) { +// L.e(t, "cannot load class " + className); + } + } + // expr visitor may call this to resolve statics. Sometimes, it is OK not to find a class. + if (loaded == null) { + return null; + } + L.d("loaded class %s", loaded.mClass.getCanonicalName()); + return loaded; + } + + @Override + public ModelClass findClass(Class classType) { + return new JavaClass(classType); + } + + @Override + public TypeUtil createTypeUtil() { + return new JavaTypeUtil(); + } + + private JavaClass loadRecursively(String className) throws ClassNotFoundException { + try { + L.d("recursively checking %s", className); + return new JavaClass(mClassLoader.loadClass(className)); + } catch (ClassNotFoundException ex) { + int lastIndexOfDot = className.lastIndexOf("."); + if (lastIndexOfDot == -1) { + throw ex; + } + return loadRecursively(className.substring(0, lastIndexOfDot) + "$" + className + .substring(lastIndexOfDot + 1)); + } + } + + @Override + public List getResources(String name) { + List urlList = new ArrayList(); + Enumeration urls = null; + try { + urls = mClassLoader.getResources(name); + if (urls != null) { + while (urls.hasMoreElements()) { + urlList.add(urls.nextElement()); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return urlList; + } + + public static void initForTests() { + Map env = System.getenv(); + for (Map.Entry entry : env.entrySet()) { + L.d("%s %s", entry.getKey(), entry.getValue()); + } + String androidHome = env.get("ANDROID_HOME"); + if (androidHome == null) { + throw new IllegalStateException("you need to have ANDROID_HOME set in your environment" + + " to run compiler tests"); + } + File androidJar = new File(androidHome + "/platforms/android-21/android.jar"); + if (!androidJar.exists() || !androidJar.canRead()) { + throw new IllegalStateException( + "cannot find android jar at " + androidJar.getAbsolutePath()); + } + // now load android data binding library as well + + try { + ClassLoader classLoader = new URLClassLoader(new URL[]{androidJar.toURI().toURL()}, + ModelAnalyzer.class.getClassLoader()); + new JavaAnalyzer(classLoader, true); + } catch (MalformedURLException e) { + throw new RuntimeException("cannot create class loader", e); + } + + SdkUtil.initialize(8, new File(androidHome)); + } +} diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaClass.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaClass.java new file mode 100644 index 0000000000000..2ea2d2d7caea0 --- /dev/null +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaClass.java @@ -0,0 +1,245 @@ +/* + * 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 com.android.databinding.reflection.java; + +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.reflection.SdkUtil; +import com.android.databinding.reflection.TypeUtil; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class JavaClass implements ModelClass { + public final Class mClass; + + public JavaClass(Class clazz) { + mClass = clazz; + } + + @Override + public String toJavaCode() { + return toJavaCode(mClass); + } + + private static String toJavaCode(Class aClass) { + if (aClass.isArray()) { + Class component = aClass.getComponentType(); + return toJavaCode(component) + "[]"; + } else { + return aClass.getCanonicalName().replace('$', '.'); + } + } + + @Override + public boolean isArray() { + return mClass.isArray(); + } + + @Override + public ModelClass getComponentType() { + if (mClass.isArray()) { + return new JavaClass(mClass.getComponentType()); + } else if (isList() || isMap()) { + return new JavaClass(Object.class); + } else { + return null; + } + } + + @Override + public boolean isList() { + return List.class.isAssignableFrom(mClass); + } + + @Override + public boolean isMap() { + return Map.class.isAssignableFrom(mClass); + } + + @Override + public boolean isString() { + return String.class.equals(mClass); + } + + @Override + public boolean isNullable() { + return Object.class.isAssignableFrom(mClass); + } + + @Override + public boolean isPrimitive() { + return mClass.isPrimitive(); + } + + @Override + public boolean isBoolean() { + return boolean.class.equals(mClass); + } + + @Override + public boolean isChar() { + return char.class.equals(mClass); + } + + @Override + public boolean isByte() { + return byte.class.equals(mClass); + } + + @Override + public boolean isShort() { + return short.class.equals(mClass); + } + + @Override + public boolean isInt() { + return int.class.equals(mClass); + } + + @Override + public boolean isLong() { + return long.class.equals(mClass); + } + + @Override + public boolean isFloat() { + return float.class.equals(mClass); + } + + @Override + public boolean isDouble() { + return double.class.equals(mClass); + } + + @Override + public boolean isObject() { + return Object.class.equals(mClass); + } + + @Override + public boolean isVoid() { + return void.class.equals(mClass); + } + + @Override + public ModelClass unbox() { + if (mClass.isPrimitive()) { + return this; + } + if (Integer.class.equals(mClass)) { + return new JavaClass(int.class); + } else if (Long.class.equals(mClass)) { + return new JavaClass(long.class); + } else if (Short.class.equals(mClass)) { + return new JavaClass(short.class); + } else if (Byte.class.equals(mClass)) { + return new JavaClass(byte.class); + } else if (Character.class.equals(mClass)) { + return new JavaClass(char.class); + } else if (Double.class.equals(mClass)) { + return new JavaClass(double.class); + } else if (Float.class.equals(mClass)) { + return new JavaClass(float.class); + } else if (Boolean.class.equals(mClass)) { + return new JavaClass(boolean.class); + } else { + // not a boxed type + return this; + } + + } + + @Override + public JavaClass box() { + if (!mClass.isPrimitive()) { + return this; + } + if (int.class.equals(mClass)) { + return new JavaClass(Integer.class); + } else if (long.class.equals(mClass)) { + return new JavaClass(Long.class); + } else if (short.class.equals(mClass)) { + return new JavaClass(Short.class); + } else if (byte.class.equals(mClass)) { + return new JavaClass(Byte.class); + } else if (char.class.equals(mClass)) { + return new JavaClass(Character.class); + } else if (double.class.equals(mClass)) { + return new JavaClass(Double.class); + } else if (float.class.equals(mClass)) { + return new JavaClass(Float.class); + } else if (boolean.class.equals(mClass)) { + return new JavaClass(Boolean.class); + } else { + // not a valid type? + return this; + } + } + + @Override + public boolean isAssignableFrom(ModelClass that) { + Class thatClass = ((JavaClass) that).mClass; + return mClass.isAssignableFrom(thatClass); + } + + @Override + public ModelMethod[] getMethods(String name, int numParameters) { + Method[] methods = mClass.getMethods(); + ArrayList matching = new ArrayList<>(); + for (Method method : methods) { + if (method.getName().equals(name) && + method.getParameterTypes().length == numParameters) { + matching.add(new JavaMethod(method)); + } + } + return matching.toArray(new JavaMethod[matching.size()]); + } + + @Override + public ModelClass getSuperclass() { + return new JavaClass(mClass.getSuperclass()); + } + + @Override + public String getCanonicalName() { + return mClass.getCanonicalName(); + } + + @Override + public int getMinApi() { + return SdkUtil.getMinApi(this); + } + + @Override + public String getJniDescription() { + return TypeUtil.getInstance().getDescription(this); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof JavaClass) { + return mClass.equals(((JavaClass) obj).mClass); + } else { + return false; + } + } + + @Override + public int hashCode() { + return mClass.hashCode(); + } +} diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockBindable.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaField.java similarity index 68% rename from tools/data-binding/compiler/src/test/java/com/android/databinding/MockBindable.java rename to tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaField.java index 9d78f0ba773aa..122bf32e5ade2 100644 --- a/tools/data-binding/compiler/src/test/java/com/android/databinding/MockBindable.java +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaField.java @@ -11,11 +11,16 @@ * limitations under the License. */ -package com.android.databinding; +package com.android.databinding.reflection.java; -import java.lang.annotation.ElementType; -import java.lang.annotation.Target; +import com.android.databinding.reflection.ModelField; -@Target({ElementType.FIELD, ElementType.METHOD}) -public @interface MockBindable { +import java.lang.reflect.Field; + +public class JavaField implements ModelField { + public final Field mField; + + public JavaField(Field field) { + mField = field; + } } diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaMethod.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaMethod.java new file mode 100644 index 0000000000000..901b045581e9d --- /dev/null +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaMethod.java @@ -0,0 +1,78 @@ +/* + * 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 com.android.databinding.reflection.java; + + +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.reflection.SdkUtil; +import com.android.databinding.reflection.TypeUtil; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; + +public class JavaMethod implements ModelMethod { + public final Method mMethod; + + public JavaMethod(Method method) { + mMethod = method; + } + + + @Override + public ModelClass getDeclaringClass() { + return new JavaClass(mMethod.getDeclaringClass()); + } + + @Override + public ModelClass[] getParameterTypes() { + Class[] parameterTypes = mMethod.getParameterTypes(); + ModelClass[] parameterClasses = new ModelClass[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + parameterClasses[i] = new JavaClass(parameterTypes[i]); + } + return parameterClasses; + } + + @Override + public String getName() { + return mMethod.getName(); + } + + @Override + public ModelClass getReturnType(List args) { + return new JavaClass(mMethod.getReturnType()); + } + + @Override + public boolean isPublic() { + return Modifier.isPublic(mMethod.getModifiers()); + } + + @Override + public boolean isStatic() { + return Modifier.isStatic(mMethod.getModifiers()); + } + + @Override + public int getMinApi() { + return SdkUtil.getMinApi(this); + } + + @Override + public String getJniDescription() { + return TypeUtil.getInstance().getDescription(this); + } +} diff --git a/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaTypeUtil.java b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaTypeUtil.java new file mode 100644 index 0000000000000..50205ef9111b6 --- /dev/null +++ b/tools/data-binding/compiler/src/test/java/com/android/databinding/reflection/java/JavaTypeUtil.java @@ -0,0 +1,89 @@ +/* + * 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 com.android.databinding.reflection.java; + +import com.android.databinding.reflection.ModelClass; +import com.android.databinding.reflection.ModelMethod; +import com.android.databinding.reflection.TypeUtil; +import com.android.databinding.util.L; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; + +public class JavaTypeUtil extends TypeUtil { + + @Override + public String getDescription(ModelClass modelClass) { + return modelClass.getCanonicalName().replace('.', '/'); + } + + @Override + public String getDescription(ModelMethod modelMethod) { + Method method = ((JavaMethod) modelMethod).mMethod; + StringBuilder sb = new StringBuilder(); + sb.append(method.getName()); + sb.append("("); + for (Class param : method.getParameterTypes()) { + sb.append(getDescription(param)); + } + sb.append(")"); + sb.append(getDescription(method.getReturnType())); + return sb.toString(); + } + + private String getDescription(Class klass) { + if (klass == null) { + throw new UnsupportedOperationException(); + } + if (boolean.class.equals(klass)) { + return BOOLEAN; + } + if (byte.class.equals(klass)) { + return BYTE; + } + if (short.class.equals(klass)) { + return SHORT; + } + if (int.class.equals(klass)) { + return INT; + } + if (long.class.equals(klass)) { + return LONG; + } + if (char.class.equals(klass)) { + return CHAR; + } + if (float.class.equals(klass)) { + return FLOAT; + } + if (double.class.equals(klass)) { + return DOUBLE; + } + if (void.class.equals(klass)) { + return VOID; + } + if (Object.class.isAssignableFrom(klass)) { + return CLASS_PREFIX + klass.getCanonicalName().replace('.', '/') + CLASS_SUFFIX; + } + if (Array.class.isAssignableFrom(klass)) { + return ARRAY + getDescription(klass.getComponentType()); + } + + UnsupportedOperationException ex + = new UnsupportedOperationException("cannot understand type " + + klass.toString() + ", kind:"); + L.e(ex, "cannot create JNI type for %s", klass.getCanonicalName()); + throw ex; + } +} diff --git a/tools/data-binding/gradlePlugin/src/main/kotlin/plugin.kt b/tools/data-binding/gradlePlugin/src/main/kotlin/plugin.kt index 2b7a20cd4431f..5913a553b7823 100644 --- a/tools/data-binding/gradlePlugin/src/main/kotlin/plugin.kt +++ b/tools/data-binding/gradlePlugin/src/main/kotlin/plugin.kt @@ -51,6 +51,7 @@ import org.apache.commons.io.IOUtils import java.io.FileWriter import java.io.ByteArrayOutputStream import org.apache.commons.codec.binary.Base64 +import com.android.builder.model.ApiVersion class DataBinderPlugin : Plugin { @@ -78,6 +79,8 @@ class DataBinderPlugin : Plugin { var viewBinderSource : File by Delegates.notNull() + var sdkDir : File by Delegates.notNull() + val viewBinderSourceRoot by Delegates.lazy { File(project.getBuildDir(), "databinder") } @@ -118,6 +121,8 @@ class DataBinderPlugin : Plugin { fun createXmlProcessor(p: Project): LayoutXmlProcessor { val ss = p.getExtensions().getByName("android") as AppExtension + sdkDir = ss.getSdkDirectory() + val minSdkVersion = ss.getDefaultConfig().getMinSdkVersion() androidJar = File(ss.getSdkDirectory().getAbsolutePath() + "/platforms/${ss.getCompileSdkVersion()}/android.jar") log("creating parser!") log("project build dir:${p.getBuildDir()}") @@ -128,10 +133,7 @@ class DataBinderPlugin : Plugin { variantData = field.get(appVariant) as ApplicationVariantData - // TODO val packageName = variantData.generateRClassTask.getPackageForR() - //"com.com.android.databinding.android.databinding.libraryGen"//variantData.getPackageName() - // val sources = variantData.getJavaSources() sources.forEach({ log("source: ${it}"); @@ -168,7 +170,7 @@ class DataBinderPlugin : Plugin { dexTask.doFirst(MethodClosure(this, "preDexAnalysis")) val writerOutBase = codeGenTargetFolder.getAbsolutePath(); fileWriter = GradleFileWriter(writerOutBase) - return LayoutXmlProcessor(packageName, resourceFolders, fileWriter) + return LayoutXmlProcessor(packageName, resourceFolders, fileWriter, minSdkVersion.getApiLevel()) } @@ -195,6 +197,6 @@ class DataBinderPlugin : Plugin { } fun generateIntermediateFile(o: Any?) { - xmlProcessor.writeIntermediateFile() + xmlProcessor.writeIntermediateFile(sdkDir) } } diff --git a/tools/data-binding/library/build.gradle b/tools/data-binding/library/build.gradle index 452bc0e5baa89..229d3e14b4950 100644 --- a/tools/data-binding/library/build.gradle +++ b/tools/data-binding/library/build.gradle @@ -69,17 +69,14 @@ dependencies { android.libraryVariants.all { variant -> def name = variant.buildType.name - if (name.equals("debug")) { + if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) { return; // Skip debug builds. } - def suffix = name.capitalize() - -// def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) { -// classifier = 'sources' -// from android.sourceSets.main.java -// } -// -// artifacts.add('archives', sourcesJarTask); + // @Jar version is needed to run compiler tests + def task = project.tasks.create "jar${name.capitalize()}", Jar + task.dependsOn variant.javaCompile + task.from variant.javaCompile.destinationDir + artifacts.add('archives', task); } uploadArchives { repositories { diff --git a/tools/data-binding/library/src/main/java/com/android/databinding/library/DataBinder.java b/tools/data-binding/library/src/main/java/com/android/databinding/library/DataBinder.java index 92549c73d53f5..4ebecefaee831 100644 --- a/tools/data-binding/library/src/main/java/com/android/databinding/library/DataBinder.java +++ b/tools/data-binding/library/src/main/java/com/android/databinding/library/DataBinder.java @@ -17,6 +17,7 @@ package com.android.databinding.library; import android.content.Context; +import android.os.Build; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; @@ -30,6 +31,12 @@ public class DataBinder { static DataBinderMapper sMapper; + /** + * Instead of directly accessing Build.VERSION.SDK_INT, generated code uses this value so that + * we can test API dependent behavior. + */ + static int SDK_INT = Build.VERSION.SDK_INT; + private WeakHashMap mDataBinderMap = new WeakHashMap<>(); private SparseArray> mDataBinderById = new SparseArray<>(); @@ -49,6 +56,10 @@ public class DataBinder { return sMapper; } + public static int getBuildSdkInt() { + return SDK_INT; + } + public static int convertToId(String key) { return getMapper().getId(key); }