Merge "Print encoded errors only if data binding is invoked from the IDE" into mnc-dev
diff --git a/compilationTests/src/test/java/android/databinding/compilationTest/BaseCompilationTest.java b/compilationTests/src/test/java/android/databinding/compilationTest/BaseCompilationTest.java
index 7682b74..c9cf2a0 100644
--- a/compilationTests/src/test/java/android/databinding/compilationTest/BaseCompilationTest.java
+++ b/compilationTests/src/test/java/android/databinding/compilationTest/BaseCompilationTest.java
@@ -18,6 +18,7 @@
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TestName;
@@ -60,6 +61,11 @@
public static final String KEY_DEPENDENCIES = "DEPENDENCIES";
public static final String KEY_SETTINGS_INCLUDES = "SETTINGS_INCLUDES";
public static final String DEFAULT_APP_PACKAGE = "com.android.databinding.compilationTest.test";
+ public static final String KEY_CLASS_NAME = "CLASSNAME";
+ public static final String KEY_CLASS_TYPE = "CLASSTYPE";
+ public static final String KEY_IMPORT_TYPE = "IMPORTTYPE";
+ public static final String KEY_INCLUDE_ID = "INCLUDEID";
+ public static final String KEY_VIEW_ID = "VIEWID";
protected final File testFolder = new File("./build/build-test");
@@ -67,6 +73,11 @@
copyResourceTo(name, new File(testFolder, path));
}
+ protected void copyResourceTo(String name, String path, Map<String, String> replacements)
+ throws IOException {
+ copyResourceTo(name, new File(testFolder, path), replacements);
+ }
+
protected void copyResourceDirectory(String name, String targetPath)
throws URISyntaxException, IOException {
URL dir = getClass().getResource(name);
@@ -236,6 +247,10 @@
Collections.addAll(args, params);
ProcessBuilder builder = new ProcessBuilder(args);
builder.environment().putAll(System.getenv());
+ String javaHome = System.getProperty("java.home");
+ if (StringUtils.isNotBlank(javaHome)) {
+ builder.environment().put("JAVA_HOME", javaHome);
+ }
builder.directory(testFolder);
Process process = builder.start();
String output = IOUtils.toString(process.getInputStream());
diff --git a/compilationTests/src/test/java/android/databinding/compilationTest/MultiLayoutVerificationTest.java b/compilationTests/src/test/java/android/databinding/compilationTest/MultiLayoutVerificationTest.java
new file mode 100644
index 0000000..0591ea9
--- /dev/null
+++ b/compilationTests/src/test/java/android/databinding/compilationTest/MultiLayoutVerificationTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.compilationTest;
+
+import org.junit.Test;
+
+import android.databinding.tool.processing.ErrorMessages;
+import android.databinding.tool.processing.ScopedErrorReport;
+import android.databinding.tool.processing.ScopedException;
+import android.databinding.tool.store.Location;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class MultiLayoutVerificationTest extends BaseCompilationTest {
+ @Test
+ public void testMultipleLayoutFilesWithNameMismatch()
+ throws IOException, URISyntaxException, InterruptedException {
+ prepareProject();
+ copyResourceTo("/layout/layout_with_class_name.xml",
+ "/app/src/main/res/layout/with_class_name.xml", toMap(KEY_CLASS_NAME,
+ "AClassName"));
+ copyResourceTo("/layout/layout_with_class_name.xml",
+ "/app/src/main/res/layout-land/with_class_name.xml", toMap(KEY_CLASS_NAME,
+ "SomeOtherClassName"));
+ CompilationResult result = runGradle("assembleDebug");
+ assertNotEquals(result.output, 0, result.resultCode);
+ List<ScopedException> exceptions = result.getBindingExceptions();
+ assertEquals(result.error, 2, exceptions.size());
+ boolean foundNormal = false;
+ boolean foundLandscape = false;
+ for (ScopedException exception : exceptions) {
+ ScopedErrorReport report = exception.getScopedErrorReport();
+ assertNotNull(report);
+ File file = new File(report.getFilePath());
+ assertTrue(file.exists());
+ assertEquals(1, report.getLocations().size());
+ Location location = report.getLocations().get(0);
+ switch (file.getParentFile().getName()) {
+ case "layout":
+ assertEquals(new File(testFolder,
+ "/app/src/main/res/layout/with_class_name.xml")
+ .getCanonicalFile(), file.getCanonicalFile());
+ String extract = extract("/app/src/main/res/layout/with_class_name.xml",
+ location);
+ assertEquals(extract, "AClassName");
+ assertEquals(String.format(
+ ErrorMessages.MULTI_CONFIG_LAYOUT_CLASS_NAME_MISMATCH,
+ DEFAULT_APP_PACKAGE + ".databinding.AClassName",
+ "layout/with_class_name"), exception.getBareMessage());
+ foundNormal = true;
+ break;
+ case "layout-land":
+ assertEquals(new File(testFolder,
+ "/app/src/main/res/layout-land/with_class_name.xml")
+ .getCanonicalFile(), file.getCanonicalFile());
+ extract = extract("/app/src/main/res/layout-land/with_class_name.xml",
+ location);
+ assertEquals("SomeOtherClassName", extract);
+ assertEquals(String.format(
+ ErrorMessages.MULTI_CONFIG_LAYOUT_CLASS_NAME_MISMATCH,
+ DEFAULT_APP_PACKAGE + ".databinding.SomeOtherClassName",
+ "layout-land/with_class_name"), exception.getBareMessage());
+ foundLandscape = true;
+ break;
+ default:
+ fail("unexpected error file");
+ }
+ }
+ assertTrue(result.error, foundNormal);
+ assertTrue(result.error, foundLandscape);
+ }
+
+ @Test
+ public void testMultipleLayoutFilesVariableMismatch()
+ throws IOException, URISyntaxException, InterruptedException {
+ prepareProject();
+ copyResourceTo("/layout/layout_with_variable_type.xml",
+ "/app/src/main/res/layout/layout_with_variable_type.xml", toMap(KEY_CLASS_TYPE,
+ "String"));
+ copyResourceTo("/layout/layout_with_variable_type.xml",
+ "/app/src/main/res/layout-land/layout_with_variable_type.xml", toMap(KEY_CLASS_TYPE,
+ "CharSequence"));
+ CompilationResult result = runGradle("assembleDebug");
+ assertNotEquals(result.output, 0, result.resultCode);
+ List<ScopedException> exceptions = result.getBindingExceptions();
+ assertEquals(result.error, 2, exceptions.size());
+ boolean foundNormal = false;
+ boolean foundLandscape = false;
+ for (ScopedException exception : exceptions) {
+ ScopedErrorReport report = exception.getScopedErrorReport();
+ assertNotNull(report);
+ File file = new File(report.getFilePath());
+ assertTrue(file.exists());
+ assertEquals(result.error, 1, report.getLocations().size());
+ Location location = report.getLocations().get(0);
+ // validated in switch
+ String config = file.getParentFile().getName();
+ String type = "???";
+ switch (file.getParentFile().getName()) {
+ case "layout":
+ type = "String";
+ foundNormal = true;
+ break;
+ case "layout-land":
+ type = "CharSequence";
+ foundLandscape = true;
+ break;
+ default:
+ fail("unexpected error file");
+ }
+ assertEquals(new File(testFolder,
+ "/app/src/main/res/" + config + "/layout_with_variable_type.xml")
+ .getCanonicalFile(), file.getCanonicalFile());
+ String extract = extract("/app/src/main/res/" + config +
+ "/layout_with_variable_type.xml", location);
+ assertEquals(extract, "<variable name=\"myVariable\" type=\"" + type + "\"/>");
+ assertEquals(String.format(
+ ErrorMessages.MULTI_CONFIG_VARIABLE_TYPE_MISMATCH,
+ "myVariable", type,
+ config + "/layout_with_variable_type"), exception.getBareMessage());
+ }
+ assertTrue(result.error, foundNormal);
+ assertTrue(result.error, foundLandscape);
+ }
+
+ @Test
+ public void testMultipleLayoutFilesImportMismatch()
+ throws IOException, URISyntaxException, InterruptedException {
+ prepareProject();
+ String typeNormal = "java.util.List";
+ String typeLand = "java.util.Map";
+ copyResourceTo("/layout/layout_with_import_type.xml",
+ "/app/src/main/res/layout/layout_with_import_type.xml", toMap(KEY_IMPORT_TYPE,
+ typeNormal));
+ copyResourceTo("/layout/layout_with_import_type.xml",
+ "/app/src/main/res/layout-land/layout_with_import_type.xml", toMap(KEY_IMPORT_TYPE,
+ typeLand));
+ CompilationResult result = runGradle("assembleDebug");
+ assertNotEquals(result.output, 0, result.resultCode);
+ List<ScopedException> exceptions = result.getBindingExceptions();
+ assertEquals(result.error, 2, exceptions.size());
+ boolean foundNormal = false;
+ boolean foundLandscape = false;
+ for (ScopedException exception : exceptions) {
+ ScopedErrorReport report = exception.getScopedErrorReport();
+ assertNotNull(report);
+ File file = new File(report.getFilePath());
+ assertTrue(file.exists());
+ assertEquals(result.error, 1, report.getLocations().size());
+ Location location = report.getLocations().get(0);
+ // validated in switch
+ String config = file.getParentFile().getName();
+ String type = "???";
+ switch (file.getParentFile().getName()) {
+ case "layout":
+ type = typeNormal;
+ foundNormal = true;
+ break;
+ case "layout-land":
+ type = typeLand;
+ foundLandscape = true;
+ break;
+ default:
+ fail("unexpected error file");
+ }
+ assertEquals(new File(testFolder,
+ "/app/src/main/res/" + config + "/layout_with_import_type.xml")
+ .getCanonicalFile(), file.getCanonicalFile());
+ String extract = extract("/app/src/main/res/" + config + "/layout_with_import_type.xml",
+ location);
+ assertEquals(extract, "<import alias=\"Blah\" type=\"" + type + "\"/>");
+ assertEquals(String.format(
+ ErrorMessages.MULTI_CONFIG_IMPORT_TYPE_MISMATCH,
+ "Blah", type,
+ config + "/layout_with_import_type"), exception.getBareMessage());
+ }
+ assertTrue(result.error, foundNormal);
+ assertTrue(result.error, foundLandscape);
+ }
+
+ @Test
+ public void testSameIdInIncludeAndView()
+ throws IOException, URISyntaxException, InterruptedException {
+ prepareProject();
+ copyResourceTo("/layout/basic_layout.xml",
+ "/app/src/main/res/layout/basic_layout.xml");
+ copyResourceTo("/layout/layout_with_include.xml",
+ "/app/src/main/res/layout/foo.xml", toMap(KEY_INCLUDE_ID, "sharedId"));
+ copyResourceTo("/layout/layout_with_view_id.xml",
+ "/app/src/main/res/layout-land/foo.xml", toMap(KEY_VIEW_ID, "sharedId"));
+ CompilationResult result = runGradle("assembleDebug");
+ assertNotEquals(result.output, 0, result.resultCode);
+ List<ScopedException> exceptions = result.getBindingExceptions();
+ assertEquals(result.error, 2, exceptions.size());
+
+ boolean foundNormal = false;
+ boolean foundLandscape = false;
+ for (ScopedException exception : exceptions) {
+ ScopedErrorReport report = exception.getScopedErrorReport();
+ assertNotNull(report);
+ File file = new File(report.getFilePath());
+ assertTrue(file.exists());
+ assertEquals(result.error, 1, report.getLocations().size());
+ Location location = report.getLocations().get(0);
+ // validated in switch
+ String config = file.getParentFile().getName();
+ switch (file.getParentFile().getName()) {
+ case "layout":
+ String extract = extract("/app/src/main/res/" + config + "/foo.xml", location);
+ assertEquals(extract, "<include layout=\"@layout/basic_layout\" "
+ + "android:id=\"@+id/sharedId\" bind:myVariable=\"@{myVariable}\"/>");
+ foundNormal = true;
+ break;
+ case "layout-land":
+ extract = extract("/app/src/main/res/" + config + "/foo.xml", location);
+ assertEquals(extract, "<TextView android:layout_width=\"wrap_content\" "
+ + "android:layout_height=\"wrap_content\" android:id=\"@+id/sharedId\" "
+ + "android:text=\"@{myVariable}\"/>");
+ foundLandscape = true;
+ break;
+ default:
+ fail("unexpected error file");
+ }
+ assertEquals(new File(testFolder,
+ "/app/src/main/res/" + config + "/foo.xml").getCanonicalFile(),
+ file.getCanonicalFile());
+ assertEquals(String.format(
+ ErrorMessages.MULTI_CONFIG_ID_USED_AS_IMPORT, "@+id/sharedId"),
+ exception.getBareMessage());
+ }
+ assertTrue(result.error, foundNormal);
+ assertTrue(result.error, foundLandscape);
+ }
+
+
+}
diff --git a/compilationTests/src/test/resources/layout/invalid_setter_binding.xml b/compilationTests/src/test/resources/layout/invalid_setter_binding.xml
new file mode 100644
index 0000000..f1b55ce
--- /dev/null
+++ b/compilationTests/src/test/resources/layout/invalid_setter_binding.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:bind="http://schemas.android.com/apk/res-auto">
+ <data>
+ <variable name="myVariable" type="String"/>
+ </data>
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- undefined variable -->
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:id="@+id/outerTextView"
+ android:textx="@{myVariable}"/>
+ </LinearLayout>
+</layout>
\ No newline at end of file
diff --git a/compilationTests/src/test/resources/layout/invalid_variable_type.xml b/compilationTests/src/test/resources/layout/invalid_variable_type.xml
new file mode 100644
index 0000000..237633a
--- /dev/null
+++ b/compilationTests/src/test/resources/layout/invalid_variable_type.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:bind="http://schemas.android.com/apk/res-auto">
+ <data>
+ <variable name="myVariable" type="Stringx"/>
+ </data>
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- undefined variable -->
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:id="@+id/outerTextView"
+ android:textx="@{myVariable}"/>
+ </LinearLayout>
+</layout>
\ No newline at end of file
diff --git a/compilationTests/src/test/resources/layout/layout_with_class_name.xml b/compilationTests/src/test/resources/layout/layout_with_class_name.xml
new file mode 100644
index 0000000..558cda9
--- /dev/null
+++ b/compilationTests/src/test/resources/layout/layout_with_class_name.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:bind="http://schemas.android.com/apk/res-auto">
+ <data class="!@{CLASSNAME}">
+ <variable name="myVariable" type="String"/>
+ </data>
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- undefined variable -->
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:id="@+id/outerTextView"
+ android:text="@{myVariable}"/>
+ </LinearLayout>
+</layout>
\ No newline at end of file
diff --git a/compilationTests/src/test/resources/layout/layout_with_import_type.xml b/compilationTests/src/test/resources/layout/layout_with_import_type.xml
new file mode 100644
index 0000000..a8e9a4b
--- /dev/null
+++ b/compilationTests/src/test/resources/layout/layout_with_import_type.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:bind="http://schemas.android.com/apk/res-auto">
+ <data>
+ <variable name="myVariable" type="String"/>
+ <import alias="Blah" type="!@{IMPORTTYPE}"/>
+ </data>
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- undefined variable -->
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:id="@+id/outerTextView"
+ android:text="@{myVariable}"/>
+ </LinearLayout>
+</layout>
\ No newline at end of file
diff --git a/compilationTests/src/test/resources/layout/layout_with_include.xml b/compilationTests/src/test/resources/layout/layout_with_include.xml
new file mode 100644
index 0000000..2eaef57
--- /dev/null
+++ b/compilationTests/src/test/resources/layout/layout_with_include.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:bind="http://schemas.android.com/apk/res-auto">
+ <data>
+ <variable name="myVariable" type="String"/>
+ </data>
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- undefined variable -->
+ <include layout="@layout/basic_layout" android:id="@+id/!@{INCLUDEID}" bind:myVariable="@{myVariable}"/>
+ </LinearLayout>
+</layout>
\ No newline at end of file
diff --git a/compilationTests/src/test/resources/layout/layout_with_variable_type.xml b/compilationTests/src/test/resources/layout/layout_with_variable_type.xml
new file mode 100644
index 0000000..3e71f37
--- /dev/null
+++ b/compilationTests/src/test/resources/layout/layout_with_variable_type.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:bind="http://schemas.android.com/apk/res-auto">
+ <data>
+ <variable name="myVariable" type="!@{CLASSTYPE}"/>
+ </data>
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- undefined variable -->
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:id="@+id/outerTextView"
+ android:text="@{myVariable}"/>
+ </LinearLayout>
+</layout>
\ No newline at end of file
diff --git a/compilationTests/src/test/resources/layout/layout_with_view_id.xml b/compilationTests/src/test/resources/layout/layout_with_view_id.xml
new file mode 100644
index 0000000..21dc440
--- /dev/null
+++ b/compilationTests/src/test/resources/layout/layout_with_view_id.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:bind="http://schemas.android.com/apk/res-auto">
+ <data>
+ <variable name="myVariable" type="String"/>
+ </data>
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- undefined variable -->
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/!@{VIEWID}" android:text="@{myVariable}"/>
+ </LinearLayout>
+</layout>
\ No newline at end of file
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java b/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
index d1d103d..a4ed1cb 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
@@ -25,4 +25,15 @@
"Cannot find the setter for attribute '%s' with parameter type %s.";
public static final String CANNOT_RESOLVE_TYPE =
"Cannot resolve type for %s";
+ public static final String MULTI_CONFIG_LAYOUT_CLASS_NAME_MISMATCH =
+ "Classname (%s) does not match the class name defined for layout(%s) in other"
+ + " configurations";
+ public static final String MULTI_CONFIG_VARIABLE_TYPE_MISMATCH =
+ "Variable declaration (%s - %s) does not match the type defined for layout(%s) in other"
+ + " configurations";
+ public static final String MULTI_CONFIG_IMPORT_TYPE_MISMATCH =
+ "Import declaration (%s - %s) does not match the import defined for layout(%s) in other"
+ + " configurations";
+ public static final String MULTI_CONFIG_ID_USED_AS_IMPORT =
+ "Cannot use the same id (%s) for a View and an include tag.";
}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java b/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java
index 723fd0a..35668cd 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java
@@ -35,6 +35,14 @@
private static ThreadLocal<ScopeEntry> sScopeItems = new ThreadLocal<ScopeEntry>();
static List<ScopedException> sDeferredExceptions = new ArrayList<>();
+ public static void enter(final Location location) {
+ enter(new LocationScopeProvider() {
+ @Override
+ public List<Location> provideScopeLocation() {
+ return Arrays.asList(location);
+ }
+ });
+ }
public static void enter(ScopeProvider scopeProvider) {
ScopeEntry peek = sScopeItems.get();
ScopeEntry entry = new ScopeEntry(scopeProvider, peek);
@@ -51,6 +59,39 @@
sDeferredExceptions.add(exception);
}
+ private static void registerErrorInternal(String msg, int scopeIndex,
+ ScopeProvider... scopeProviders) {
+ if (scopeProviders == null || scopeProviders.length <= scopeIndex) {
+ defer(new ScopedException(msg));
+ } else if (scopeProviders[scopeIndex] == null) {
+ registerErrorInternal(msg, scopeIndex + 1, scopeProviders);
+ } else {
+ try {
+ Scope.enter(scopeProviders[scopeIndex]);
+ registerErrorInternal(msg, scopeIndex + 1, scopeProviders);
+ } finally {
+ Scope.exit();
+ }
+ }
+ }
+
+ /**
+ * Convenience method to add an error in a known list of scopes, w/o adding try catch flows.
+ * <p>
+ * This code actually starts entering the given scopes 1 by 1, starting from 0. When list is
+ * consumed, it creates the exception and defers if possible then exits from the provided
+ * scopes.
+ * <p>
+ * Note that these scopes are added on top of the already existing scopes.
+ *
+ * @param msg The exception message
+ * @param scopeProviders The list of additional scope providers to enter. Null scopes are
+ * automatically ignored.
+ */
+ public static void registerError(String msg, ScopeProvider... scopeProviders) {
+ registerErrorInternal(msg, 0, scopeProviders);
+ }
+
public static void assertNoError() {
if (sDeferredExceptions.isEmpty()) {
return;
diff --git a/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java b/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java
index 422cd7e..91be28f 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java
@@ -298,9 +298,18 @@
variable.toStringTree(), xml);
bundle.addVariable(name, type, new Location(variable));
}
- final String className = attributeMap(data).get("class");
- if (StringUtils.isNotBlank(className)) {
- bundle.setBindingClass(className);
+ final XMLParser.AttributeContext className = findAttribute(data, "class");
+ if (className != null) {
+ final String name = escapeQuotes(className.attrValue.getText(), true);
+ if (StringUtils.isNotBlank(name)) {
+ Location location = new Location(
+ className.attrValue.getLine() - 1,
+ className.attrValue.getCharPositionInLine() + 1,
+ className.attrValue.getLine() - 1,
+ className.attrValue.getCharPositionInLine() + name.length()
+ );
+ bundle.setBindingClass(name, location);
+ }
}
}
@@ -443,6 +452,16 @@
return result;
}
+ private static XMLParser.AttributeContext findAttribute(XMLParser.ElementContext element,
+ String name) {
+ for (XMLParser.AttributeContext attr : element.attribute()) {
+ if (escapeQuotes(attr.attrName.getText(), false).equals(name)) {
+ return attr;
+ }
+ }
+ return null;
+ }
+
private static String escapeQuotes(String textWithQuotes, boolean unescapeValue) {
char first = textWithQuotes.charAt(0);
int start = 0, end = textWithQuotes.length();
diff --git a/compilerCommon/src/main/java/android/databinding/tool/store/Location.java b/compilerCommon/src/main/java/android/databinding/tool/store/Location.java
index 3342016..d88f0de 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/store/Location.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/store/Location.java
@@ -19,6 +19,11 @@
import org.antlr.v4.runtime.Token;
import org.apache.commons.lang3.StringUtils;
+import android.databinding.tool.processing.scopes.LocationScopeProvider;
+
+import java.util.Arrays;
+import java.util.List;
+
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
@@ -223,4 +228,13 @@
into[1] = Integer.parseInt(content.substring(index + 1).trim());
return true;
}
+
+ public LocationScopeProvider createScope() {
+ return new LocationScopeProvider() {
+ @Override
+ public List<Location> provideScopeLocation() {
+ return Arrays.asList(Location.this);
+ }
+ };
+ }
}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java b/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java
index be8a4fa..74a0676 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java
@@ -13,10 +13,12 @@
package android.databinding.tool.store;
-import org.antlr.v4.runtime.Token;
import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
+import android.databinding.tool.processing.ErrorMessages;
+import android.databinding.tool.processing.Scope;
+import android.databinding.tool.processing.ScopedException;
+import android.databinding.tool.processing.scopes.FileScopeProvider;
import android.databinding.tool.processing.scopes.LocationScopeProvider;
import android.databinding.tool.util.L;
import android.databinding.tool.util.ParserHelper;
@@ -103,43 +105,30 @@
if (bundles.getValue().size() < 2) {
continue;
}
+
// validate all ids are in correct view types
// and all variables have the same name
- Map<String, NameTypeLocation> variableTypes = new HashMap<String, NameTypeLocation>();
- Map<String, NameTypeLocation> importTypes = new HashMap<String, NameTypeLocation>();
- String bindingClass = null;
-
for (LayoutFileBundle bundle : bundles.getValue()) {
bundle.mHasVariations = true;
- if (bindingClass == null) {
- bindingClass = bundle.getFullBindingClass();
- } else {
- if (!bindingClass.equals(bundle.getFullBindingClass())) {
- L.e("Binding class names must match. Layout file for %s have " +
- "different binding class names %s and %s",
- bundle.getFileName(),
- bindingClass, bundle.getFullBindingClass());
- }
- }
- for (NameTypeLocation variable : bundle.mVariables) {
- NameTypeLocation existing = variableTypes.get(variable.name);
- if (existing != null && !existing.equals(variable)) {
- L.e("inconsistent variable types for %s for layout %s",
- variable, bundle.mFileName);
- continue;
- }
- variableTypes.put(variable.name, variable);
- }
- for (NameTypeLocation userImport : bundle.mImports) {
- NameTypeLocation existing = importTypes.get(userImport.name);
- if (existing != null && !existing.equals(userImport)) {
- L.e("inconsistent variable types for %s for layout %s",
- userImport, bundle.mFileName);
- continue;
- }
- importTypes.put(userImport.name, userImport);
- }
}
+ String bindingClass = validateAndGetSharedClassName(bundles.getValue());
+ Map<String, NameTypeLocation> variableTypes = validateAndMergeNameTypeLocations(
+ bundles.getValue(), ErrorMessages.MULTI_CONFIG_VARIABLE_TYPE_MISMATCH,
+ new ValidateAndFilterCallback() {
+ @Override
+ public List<NameTypeLocation> get(LayoutFileBundle bundle) {
+ return bundle.mVariables;
+ }
+ });
+
+ Map<String, NameTypeLocation> importTypes = validateAndMergeNameTypeLocations(
+ bundles.getValue(), ErrorMessages.MULTI_CONFIG_IMPORT_TYPE_MISMATCH,
+ new ValidateAndFilterCallback() {
+ @Override
+ public List<NameTypeLocation> get(LayoutFileBundle bundle) {
+ return bundle.mImports;
+ }
+ });
for (LayoutFileBundle bundle : bundles.getValue()) {
// now add missing ones to each to ensure they can be referenced
@@ -166,65 +155,102 @@
Map<String, String> viewTypes = new HashMap<String, String>();
Map<String, String> includes = new HashMap<String, String>();
L.d("validating ids for %s", bundles.getKey());
+ Set<String> conflictingIds = new HashSet<>();
for (LayoutFileBundle bundle : bundles.getValue()) {
- for (BindingTargetBundle target : bundle.mBindingTargetBundles) {
- L.d("checking %s %s %s", target.getId(), target.getFullClassName(),
- target.isBinder());
- if (target.mId != null) {
- if (target.isBinder()) {
- if (viewBindingIds.contains(target.getFullClassName())) {
- L.e("Cannot use the same id for a View and an include tag. Error " +
- "in file %s / %s", bundle.mFileName, bundle.mConfigName);
+ try {
+ Scope.enter(bundle);
+ for (BindingTargetBundle target : bundle.mBindingTargetBundles) {
+ try {
+ Scope.enter(target);
+ L.d("checking %s %s %s", target.getId(), target.getFullClassName(),
+ target.isBinder());
+ if (target.mId != null) {
+ if (target.isBinder()) {
+ if (viewBindingIds.contains(target.mId)) {
+ L.d("%s is conflicting", target.mId);
+ conflictingIds.add(target.mId);
+ continue;
+ }
+ includeBindingIds.add(target.mId);
+ } else {
+ if (includeBindingIds.contains(target.mId)) {
+ L.d("%s is conflicting", target.mId);
+ conflictingIds.add(target.mId);
+ continue;
+ }
+ viewBindingIds.add(target.mId);
+ }
+ String existingType = viewTypes.get(target.mId);
+ if (existingType == null) {
+ L.d("assigning %s as %s", target.getId(),
+ target.getFullClassName());
+ viewTypes.put(target.mId, target.getFullClassName());
+ if (target.isBinder()) {
+ includes.put(target.mId, target.getIncludedLayout());
+ }
+ } else if (!existingType.equals(target.getFullClassName())) {
+ if (target.isBinder()) {
+ L.d("overriding %s as base binder", target.getId());
+ viewTypes.put(target.mId,
+ "android.databinding.ViewDataBinding");
+ includes.put(target.mId, target.getIncludedLayout());
+ } else {
+ L.d("overriding %s as base view", target.getId());
+ viewTypes.put(target.mId, "android.view.View");
+ }
+ }
}
- includeBindingIds.add(target.getFullClassName());
- } else {
- if (includeBindingIds.contains(target.getFullClassName())) {
- L.e("Cannot use the same id for a View and an include tag. Error in"
- + " file %s / %s", bundle.mFileName, bundle.mConfigName);
- }
- viewBindingIds.add(target.getFullClassName());
+ } catch (ScopedException ex) {
+ Scope.defer(ex);
+ } finally {
+ Scope.exit();
}
- String existingType = viewTypes.get(target.mId);
- if (existingType == null) {
- L.d("assigning %s as %s", target.getId(), target.getFullClassName());
- viewTypes.put(target.mId, target.getFullClassName());
- if (target.isBinder()) {
- includes.put(target.mId, target.getIncludedLayout());
- }
- } else if (!existingType.equals(target.getFullClassName())) {
- if (target.isBinder()) {
- L.d("overriding %s as base binder", target.getId());
- viewTypes.put(target.mId,
- "android.databinding.ViewDataBinding");
- includes.put(target.mId, target.getIncludedLayout());
- } else {
- L.d("overriding %s as base view", target.getId());
- viewTypes.put(target.mId, "android.view.View");
- }
+ }
+ } finally {
+ Scope.exit();
+ }
+ }
+
+ if (!conflictingIds.isEmpty()) {
+ for (LayoutFileBundle bundle : bundles.getValue()) {
+ for (BindingTargetBundle target : bundle.mBindingTargetBundles) {
+ if (conflictingIds.contains(target.mId)) {
+ Scope.registerError(String.format(
+ ErrorMessages.MULTI_CONFIG_ID_USED_AS_IMPORT,
+ target.mId), bundle, target);
}
}
}
}
for (LayoutFileBundle bundle : bundles.getValue()) {
- for (Map.Entry<String, String> viewType : viewTypes.entrySet()) {
- BindingTargetBundle target = bundle.getBindingTargetById(viewType.getKey());
- if (target == null) {
- String include = includes.get(viewType.getKey());
- if (include == null) {
- bundle.createBindingTarget(viewType.getKey(), viewType.getValue(),
- false, null, null, null);
+ try {
+ Scope.enter(bundle);
+ for (Map.Entry<String, String> viewType : viewTypes.entrySet()) {
+ BindingTargetBundle target = bundle.getBindingTargetById(viewType.getKey());
+ if (target == null) {
+ String include = includes.get(viewType.getKey());
+ if (include == null) {
+ bundle.createBindingTarget(viewType.getKey(), viewType.getValue(),
+ false, null, null, null);
+ } else {
+ BindingTargetBundle bindingTargetBundle = bundle
+ .createBindingTarget(
+ viewType.getKey(), null, false, null, null, null);
+ bindingTargetBundle
+ .setIncludedLayout(includes.get(viewType.getKey()));
+ bindingTargetBundle.setInterfaceType(viewType.getValue());
+ }
} else {
- BindingTargetBundle bindingTargetBundle = bundle.createBindingTarget(
- viewType.getKey(), null, false, null, null, null);
- bindingTargetBundle.setIncludedLayout(includes.get(viewType.getKey()));
- bindingTargetBundle.setInterfaceType(viewType.getValue());
+ L.d("setting interface type on %s (%s) as %s", target.mId,
+ target.getFullClassName(), viewType.getValue());
+ target.setInterfaceType(viewType.getValue());
}
- } else {
- L.d("setting interface type on %s (%s) as %s", target.mId,
- target.getFullClassName(), viewType.getValue());
- target.setInterfaceType(viewType.getValue());
}
+ } catch (ScopedException ex) {
+ Scope.defer(ex);
+ } finally {
+ Scope.exit();
}
}
}
@@ -249,9 +275,88 @@
}
}
+ /**
+ * Receives a list of bundles which are representations of the same layout file in different
+ * configurations.
+ * @param bundles
+ * @return The map for variables and their types
+ */
+ private Map<String, NameTypeLocation> validateAndMergeNameTypeLocations(
+ List<LayoutFileBundle> bundles, String errorMessage,
+ ValidateAndFilterCallback callback) {
+ Map<String, NameTypeLocation> result = new HashMap<>();
+ Set<String> mismatched = new HashSet<>();
+ for (LayoutFileBundle bundle : bundles) {
+ for (NameTypeLocation item : callback.get(bundle)) {
+ NameTypeLocation existing = result.get(item.name);
+ if (existing != null && !existing.type.equals(item.type)) {
+ mismatched.add(item.name);
+ continue;
+ }
+ result.put(item.name, item);
+ }
+ }
+ if (mismatched.isEmpty()) {
+ return result;
+ }
+ // create exceptions. We could get more clever and find the outlier but for now, listing
+ // each file w/ locations seems enough
+ for (String mismatch : mismatched) {
+ for (LayoutFileBundle bundle : bundles) {
+ NameTypeLocation found = null;
+ for (NameTypeLocation item : callback.get(bundle)) {
+ if (mismatch.equals(item.name)) {
+ found = item;
+ break;
+ }
+ }
+ if (found == null) {
+ // variable is not defined in this layout, continue
+ continue;
+ }
+ Scope.registerError(String.format(
+ errorMessage, found.name, found.type,
+ bundle.mDirectory + "/" + bundle.getFileName()), bundle,
+ found.location.createScope());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Receives a list of bundles which are representations of the same layout file in different
+ * configurations.
+ * @param bundles
+ * @return The shared class name for these bundles
+ */
+ private String validateAndGetSharedClassName(List<LayoutFileBundle> bundles) {
+ String sharedClassName = null;
+ boolean hasMismatch = false;
+ for (LayoutFileBundle bundle : bundles) {
+ bundle.mHasVariations = true;
+ String fullBindingClass = bundle.getFullBindingClass();
+ if (sharedClassName == null) {
+ sharedClassName = fullBindingClass;
+ } else if (!sharedClassName.equals(fullBindingClass)) {
+ hasMismatch = true;
+ break;
+ }
+ }
+ if (!hasMismatch) {
+ return sharedClassName;
+ }
+ // generate proper exceptions for each
+ for (LayoutFileBundle bundle : bundles) {
+ Scope.registerError(String.format(ErrorMessages.MULTI_CONFIG_LAYOUT_CLASS_NAME_MISMATCH,
+ bundle.getFullBindingClass(), bundle.mDirectory + "/" + bundle.getFileName()),
+ bundle, bundle.getClassNameLocationProvider());
+ }
+ return sharedClassName;
+ }
+
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name="Layout")
- public static class LayoutFileBundle implements Serializable {
+ public static class LayoutFileBundle implements Serializable, FileScopeProvider {
@XmlAttribute(name="layout", required = true)
public String mFileName;
@XmlAttribute(name="modulePackage", required = true)
@@ -264,6 +369,9 @@
@XmlAttribute(name="bindingClass", required = false)
public String mBindingClass;
+ // The location of the name of the generated class, optional
+ @XmlElement(name = "ClassNameLocation", required = false)
+ private Location mClassNameLocation;
// The full package and class name as determined from mBindingClass and mModulePackage
private String mFullBindingClass;
@@ -290,6 +398,8 @@
@XmlAttribute(name="isMerge", required = true)
private boolean mIsMerge;
+ private LocationScopeProvider mClassNameLocationProvider;
+
// for XML binding
public LayoutFileBundle() {
}
@@ -303,6 +413,14 @@
mAbsoluteFilePath = file.getAbsolutePath();
}
+ public LocationScopeProvider getClassNameLocationProvider() {
+ if (mClassNameLocationProvider == null && mClassNameLocation != null
+ && mClassNameLocation.isValid()) {
+ mClassNameLocationProvider = mClassNameLocation.createScope();
+ }
+ return mClassNameLocationProvider;
+ }
+
public void addVariable(String name, String type, Location location) {
Preconditions.check(!NameTypeLocation.contains(mVariables, name),
"Cannot use same variable name twice. %s in %s", name, location);
@@ -373,8 +491,9 @@
return mBindingClassName;
}
- public void setBindingClass(String bindingClass) {
+ public void setBindingClass(String bindingClass, Location location) {
mBindingClass = bindingClass;
+ mClassNameLocation = location;
}
public String getBindingClassPackage() {
@@ -459,6 +578,11 @@
public String getAbsoluteFilePath() {
return mAbsoluteFilePath;
}
+
+ @Override
+ public String provideScopeFilePath() {
+ return mAbsoluteFilePath;
+ }
}
@XmlAccessorType(XmlAccessType.NONE)
@@ -707,4 +831,11 @@
}
}
}
+
+ /**
+ * Just an inner callback class to process imports and variables w/ the same code.
+ */
+ private interface ValidateAndFilterCallback {
+ List<NameTypeLocation> get(LayoutFileBundle bundle);
+ }
}
diff --git a/integration-tests/TestApp/app/src/main/res/layout-land/multi_res_layout.xml b/integration-tests/TestApp/app/src/main/res/layout-land/multi_res_layout.xml
index 8e719d5..bb2f58d 100644
--- a/integration-tests/TestApp/app/src/main/res/layout-land/multi_res_layout.xml
+++ b/integration-tests/TestApp/app/src/main/res/layout-land/multi_res_layout.xml
@@ -14,6 +14,10 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
+
+
+
+
<data>
<variable name="objectInLand" type="android.databinding.testapp.vo.NotBindableVo"/>
</data>