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>