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); }