Merge "Add @Transaction annotation" into oc-mr1-dev
diff --git a/room/common/src/main/java/android/arch/persistence/room/Transaction.java b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
new file mode 100644
index 0000000..914e4f4
--- /dev/null
+++ b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 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.arch.persistence.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method in an abstract {@link Dao} class as a transaction method.
+ * <p>
+ * The derived implementation of the method will execute the super method in a database transaction.
+ * All the parameters and return types are preserved. The transaction will be marked as successful
+ * unless an exception is thrown in the method body.
+ * <p>
+ * Example:
+ * <pre>
+ * {@literal @}Dao
+ * public abstract class ProductDao {
+ *    {@literal @}Insert
+ *     public abstract void insert(Product product);
+ *    {@literal @}Delete
+ *     public abstract void delete(Product product);
+ *    {@literal @}Transaction
+ *     public void insertAndDeleteInTransaction(Product newProduct, Product oldProduct) {
+ *         // Anything inside this method runs in a single transaction.
+ *         insert(newProduct);
+ *         delete(oldProduct);
+ *     }
+ * }
+ * </pre>
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.CLASS)
+public @interface Transaction {
+}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
index 0c0ff49..2093d91 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
@@ -20,6 +20,7 @@
 import android.arch.persistence.room.Insert
 import android.arch.persistence.room.Query
 import android.arch.persistence.room.SkipQueryVerification
+import android.arch.persistence.room.Transaction
 import android.arch.persistence.room.Update
 import android.arch.persistence.room.ext.hasAnnotation
 import android.arch.persistence.room.ext.hasAnyOf
@@ -109,6 +110,17 @@
                     executableElement = it).process()
         } ?: emptyList()
 
+        val transactionMethods = allMembers.filter {
+            it.hasAnnotation(Transaction::class)
+                    && it.kind == ElementKind.METHOD
+            // TODO: Exclude abstract methods and let @Query handle that case
+        }.map {
+            TransactionMethodProcessor(
+                    baseContext = context,
+                    containing = declaredType,
+                    executableElement = MoreElements.asExecutable(it)).process()
+        }
+
         val constructors = allMembers
                 .filter { it.kind == ElementKind.CONSTRUCTOR }
                 .map { MoreElements.asExecutable(it) }
@@ -139,6 +151,7 @@
                 insertionMethods = insertionMethods,
                 deletionMethods = deletionMethods,
                 updateMethods = updateMethods,
+                transactionMethods = transactionMethods,
                 constructorParamType = constructorParamType)
     }
 
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
index 01a4c5f..a576aa2 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
@@ -122,6 +122,9 @@
     val UPDATE_MISSING_PARAMS = "Method annotated with" +
             " @Update but does not have any parameters to update."
 
+    val TRANSACTION_METHOD_MODIFIERS = "Method annotated with @Transaction must not be " +
+            "private, final, or abstract."
+
     val CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER = "Type of the parameter must be a class " +
             "annotated with @Entity or a collection/array of it."
 
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
new file mode 100644
index 0000000..f861ebe
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 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.arch.persistence.room.processor
+
+import android.arch.persistence.room.ext.hasAnyOf
+import android.arch.persistence.room.vo.TransactionMethod
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier.ABSTRACT
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.element.Modifier.FINAL
+import javax.lang.model.element.Modifier.PRIVATE
+
+class TransactionMethodProcessor(baseContext: Context,
+                                 val containing: DeclaredType,
+                                 val executableElement: ExecutableElement) {
+
+    val context = baseContext.fork(executableElement)
+
+    fun process(): TransactionMethod {
+        // TODO: Remove abstract check
+        context.checker.check(!executableElement.hasAnyOf(PRIVATE, FINAL, ABSTRACT),
+                executableElement, ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
+
+        return TransactionMethod(
+                element = executableElement,
+                name = executableElement.simpleName.toString())
+    }
+}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
index 227fa49..57d7598 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Dao.kt
@@ -26,6 +26,7 @@
                val insertionMethods : List<InsertionMethod>,
                val deletionMethods : List<DeletionMethod>,
                val updateMethods : List<UpdateMethod>,
+               val transactionMethods : List<TransactionMethod>,
                val constructorParamType : TypeName?) {
     // parsed dao might have a suffix if it is used in multiple databases.
     private var suffix : String? = null
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/TransactionMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/TransactionMethod.kt
new file mode 100644
index 0000000..c150aee
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/TransactionMethod.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2017 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.arch.persistence.room.vo
+
+import javax.lang.model.element.ExecutableElement
+
+class TransactionMethod(val element: ExecutableElement, val name: String)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
index a800966..5c7d8a3 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
@@ -30,6 +30,7 @@
 import android.arch.persistence.room.vo.InsertionMethod
 import android.arch.persistence.room.vo.QueryMethod
 import android.arch.persistence.room.vo.ShortcutMethod
+import android.arch.persistence.room.vo.TransactionMethod
 import com.google.auto.common.MoreTypes
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.CodeBlock
@@ -46,6 +47,7 @@
 import javax.lang.model.element.Modifier.PRIVATE
 import javax.lang.model.element.Modifier.PUBLIC
 import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind
 
 /**
  * Creates the implementation for a class annotated with Dao.
@@ -83,7 +85,7 @@
         // delete queries that must be rebuild every single time
         val oneOffDeleteOrUpdateQueries = groupedDeleteUpdate[true] ?: emptyList()
         val shortcutMethods = createInsertionMethods() +
-                createDeletionMethods() + createUpdateMethods() +
+                createDeletionMethods() + createUpdateMethods() + createTransactionMethods() +
                 createPreparedDeleteOrUpdateQueries(preparedDeleteOrUpdateQueries)
 
         builder.apply {
@@ -159,6 +161,63 @@
         return methodBuilder.build()
     }
 
+    private fun createTransactionMethods(): List<PreparedStmtQuery> {
+        return dao.transactionMethods.map {
+            PreparedStmtQuery(emptyMap(), createTransactionMethodBody(it))
+        }
+    }
+
+    private fun createTransactionMethodBody(method: TransactionMethod): MethodSpec {
+        val scope = CodeGenScope(this)
+        val methodBuilder = overrideWithoutAnnotations(method.element, declaredDao).apply {
+            addStatement("$N.beginTransaction()", dbField)
+            beginControlFlow("try").apply {
+                val returnsValue = method.element.returnType.kind != TypeKind.VOID
+                val resultVar = if (returnsValue) {
+                    scope.getTmpVar("_result")
+                } else {
+                    null
+                }
+                addDelegateToSuperStatement(method.element, resultVar)
+                addStatement("$N.setTransactionSuccessful()", dbField)
+                if (returnsValue) {
+                    addStatement("return $N", resultVar)
+                }
+            }
+            nextControlFlow("finally").apply {
+                addStatement("$N.endTransaction()", dbField)
+            }
+            endControlFlow()
+        }
+        return methodBuilder.build()
+    }
+
+    private fun MethodSpec.Builder.addDelegateToSuperStatement(element: ExecutableElement,
+                                                               result: String?) {
+        val params: MutableList<Any> = mutableListOf()
+        val format = buildString {
+            if (result != null) {
+                append("$T $L = ")
+                params.add(element.returnType)
+                params.add(result)
+            }
+            append("super.$N(")
+            params.add(element.simpleName)
+            var first = true
+            element.parameters.forEach {
+                if (first) {
+                    first = false
+                } else {
+                    append(", ")
+                }
+                append(L)
+                params.add(it.simpleName)
+            }
+            append(")")
+        }
+        addStatement(format, *params.toTypedArray())
+    }
+
     private fun createConstructor(dbParam: ParameterSpec,
                                   shortcutMethods: List<PreparedStmtQuery>,
                                   callSuper: Boolean): MethodSpec {
diff --git a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
index ca90163..89859e5 100644
--- a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
@@ -31,6 +31,11 @@
         mDb = db;
     }
 
+    @Transaction
+    public boolean transactionMethod(int i, String s, long l) {
+        return true;
+    }
+
     @Query("SELECT name || lastName as fullName, uid as id FROM user where uid = :id")
     abstract public List<FullName> fullNames(int id);
 
diff --git a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
index 87d0c49..dbb54fb 100644
--- a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -27,6 +27,18 @@
     }
 
     @Override
+    public boolean transactionMethod(int i, String s, long l) {
+        __db.beginTransaction();
+        try {
+            boolean _result = super.transactionMethod(i, s, l);
+            __db.setTransactionSuccessful();
+            return _result;
+        } finally {
+            __db.endTransaction();
+        }
+    }
+
+    @Override
     public List<ComplexDao.FullName> fullNames(int id) {
         final String _sql = "SELECT name || lastName as fullName, uid as id FROM user where uid = ?";
         final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessorTest.kt
new file mode 100644
index 0000000..a07cbe6
--- /dev/null
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessorTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2017 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.arch.persistence.room.processor
+
+import android.arch.persistence.room.Dao
+import android.arch.persistence.room.Transaction
+import android.arch.persistence.room.testing.TestInvocation
+import android.arch.persistence.room.testing.TestProcessor
+import android.arch.persistence.room.vo.TransactionMethod
+import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreTypes
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+@RunWith(JUnit4::class)
+class TransactionMethodProcessorTest {
+
+    companion object {
+        const val DAO_PREFIX = """
+                package foo.bar;
+                import android.arch.persistence.room.*;
+                import java.util.*;
+                @Dao
+                abstract class MyClass {
+                """
+        const val DAO_SUFFIX = "}"
+    }
+
+    @Test
+    fun simple() {
+        singleTransactionMethod(
+                """
+                @Transaction
+                public String doInTransaction(int param) { return null; }
+                """) { transaction, _ ->
+            assertThat(transaction.name, `is`("doInTransaction"))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun modifier_private() {
+        singleTransactionMethod(
+                """
+                @Transaction
+                private String doInTransaction(int param) { return null; }
+                """) { transaction, _ ->
+            assertThat(transaction.name, `is`("doInTransaction"))
+        }.failsToCompile().withErrorContaining(ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
+    }
+
+    @Test
+    fun modifier_final() {
+        singleTransactionMethod(
+                """
+                @Transaction
+                public final String doInTransaction(int param) { return null; }
+                """) { transaction, _ ->
+            assertThat(transaction.name, `is`("doInTransaction"))
+        }.failsToCompile().withErrorContaining(ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
+    }
+
+    private fun singleTransactionMethod(vararg input: String,
+                                handler: (TransactionMethod, TestInvocation) -> Unit):
+            CompileTester {
+        return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+                .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
+                        TransactionMethodProcessorTest.DAO_PREFIX + input.joinToString("\n") +
+                                TransactionMethodProcessorTest.DAO_SUFFIX
+                )))
+                .processedWith(TestProcessor.builder()
+                        .forAnnotations(Transaction::class, Dao::class)
+                        .nextRunHandler { invocation ->
+                            val (owner, methods) = invocation.roundEnv
+                                    .getElementsAnnotatedWith(Dao::class.java)
+                                    .map {
+                                        Pair(it,
+                                                invocation.processingEnv.elementUtils
+                                                        .getAllMembers(MoreElements.asType(it))
+                                                        .filter {
+                                                            MoreElements.isAnnotationPresent(it,
+                                                                    Transaction::class.java)
+                                                        }
+                                        )
+                                    }.filter { it.second.isNotEmpty() }.first()
+                            val processor = TransactionMethodProcessor(
+                                    baseContext = invocation.context,
+                                    containing = MoreTypes.asDeclared(owner.asType()),
+                                    executableElement = MoreElements.asExecutable(methods.first()))
+                            val processed = processor.process()
+                            handler(processed, invocation)
+                            true
+                        }
+                        .build())
+    }
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 337c233..665a1ae 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -24,6 +24,7 @@
 import android.arch.persistence.room.Insert;
 import android.arch.persistence.room.OnConflictStrategy;
 import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Transaction;
 import android.arch.persistence.room.Update;
 import android.arch.persistence.room.integration.testapp.TestDatabase;
 import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
@@ -259,4 +260,10 @@
             + " WHERE mLastName > :lastName or (mLastName = :lastName and (mName < :name or (mName = :name and mId > :id)))"
             + " ORDER BY mLastName ASC, mName DESC, mId ASC")
     public abstract int userComplexCountBefore(String lastName, String name, int id);
+
+    @Transaction
+    public void insertBothByAnnotation(final User a, final User b) {
+        insert(a);
+        insert(b);
+    }
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
index 8861adb..f8049f3 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -502,4 +502,26 @@
         assertThat(mUserDao.updateByAgeAndIds(3f, 30, Arrays.asList(3, 5)), is(1));
         assertThat(mUserDao.loadByIds(3)[0].getWeight(), is(3f));
     }
+
+    @Test
+    public void transactionByAnnotation() {
+        User a = TestUtil.createUser(3);
+        User b = TestUtil.createUser(5);
+        mUserDao.insertBothByAnnotation(a, b);
+        assertThat(mUserDao.count(), is(2));
+    }
+
+    @Test
+    public void transactionByAnnotation_failure() {
+        User a = TestUtil.createUser(3);
+        User b = TestUtil.createUser(3);
+        boolean caught = false;
+        try {
+            mUserDao.insertBothByAnnotation(a, b);
+        } catch (SQLiteConstraintException e) {
+            caught = true;
+        }
+        assertTrue("SQLiteConstraintException expected", caught);
+        assertThat(mUserDao.count(), is(0));
+    }
 }