Type Adapters

This CL introduces type adapter logic that can convert a java
type into / from a database column (ColumnTypeAdapter).

There are also type adapters which can be used to create intermediate
representations if the type cannot directly be converted into a
database column (e.g. a Boolean is first converted into int then
into database).

So far we only have primitive types + string. More will be added later.

An additional type adapter to convert int list to string is also
provided to cover more cases (might be removed).

The code generation structure may change in the future as we write
real code that reads / writes an entitiy but based on the prototype,
this setup seems promising.

Also, there is no way to define custom adapters yet, it will also come
later.

TypeAdapterStore is not optimized at all since the structure is not
set and equals checks on TypeMirror objects does not seem to work.
Once we have the final structure, may optimize.

Bug: 32342709
Test: BasicColumnTypeAdaptersTest.kt, TypeAdapterStoreTest.kt

Change-Id: I52fde5a92dbe0ebe7290e001ba9e497c6d8e3981
diff --git a/flatfoot-common/init.gradle b/flatfoot-common/init.gradle
index 2b67527..48a220e 100644
--- a/flatfoot-common/init.gradle
+++ b/flatfoot-common/init.gradle
@@ -24,7 +24,7 @@
                  "$prebuiltsRoot/../out/host/gradle/frameworks/support/build/support_repo/"]
 
 ext.kotlin_version = "1.0.5"
-ext.android_gradle_plugin_version = "2.2.0"
+ext.android_gradle_plugin_version = "2.2.1"
 ext.auto_common_version = "0.6"
 ext.javapoet_version = "1.7.0"
 ext.compile_testing_version = "0.9"
diff --git a/room/compiler/build.gradle b/room/compiler/build.gradle
index 5b536cd..746f34f 100644
--- a/room/compiler/build.gradle
+++ b/room/compiler/build.gradle
@@ -32,6 +32,10 @@
 }
 
 dependencies {
+    // take from ButterKnife
+    def logger = new com.android.build.gradle.internal.LoggerWrapper(project.logger)
+    def sdkHandler = new com.android.build.gradle.internal.SdkHandler(project, logger)
+
     compile project(":common")
     compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
     compile "com.google.auto:auto-common:$auto_common_version"
@@ -39,10 +43,10 @@
     compile 'org.antlr:antlr4:4.5.3'
 
     testCompile "com.google.testing.compile:compile-testing:$compile_testing_version"
-    testCompile files(org.gradle.internal.jvm.Jvm.current().getToolsJar())
     testCompile "junit:junit:$junit_version"
     testCompile "com.intellij:annotations:$intellij_annotation"
     testCompile "org.mockito:mockito-core:$mockito_version"
+    testCompile fileTree(dir: "${sdkHandler.sdkFolder}/platforms/android-$target_sdk_version/", include : "android.jar")
 }
 
 uploadArchives {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
new file mode 100644
index 0000000..b173508
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.ext
+
+val L = "\$L"
+val T = "\$T"
+val N = "\$N"
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/preconditions/Checks.kt b/room/compiler/src/main/kotlin/com/android/support/room/preconditions/Checks.kt
index f85488a..64619f4 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/preconditions/Checks.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/preconditions/Checks.kt
@@ -18,7 +18,6 @@
 
 import com.android.support.room.errors.ElementBoundException
 import com.android.support.room.ext.hasAnnotation
-import com.android.support.room.processor.ProcessorErrors
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
 import com.squareup.javapoet.TypeVariableName
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/CodeGenScope.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/CodeGenScope.kt
new file mode 100644
index 0000000..9ad3b49
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/CodeGenScope.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.google.common.annotations.VisibleForTesting
+import com.squareup.javapoet.CodeBlock
+
+/**
+ * Defines a code generation scope where we can provide temporary variables, global variables etc
+ */
+class CodeGenScope {
+    private var tmpVarIndex = 0
+    private var builder : CodeBlock.Builder? = null
+    companion object {
+        const val TMP_VAR_PREFIX = "_tmp"
+        @VisibleForTesting
+        fun _tmpVar(index:Int) = "${TMP_VAR_PREFIX}_$index"
+    }
+
+    fun builder() : CodeBlock.Builder {
+        if (builder == null) {
+            builder = CodeBlock.builder()
+        }
+        return builder!!
+    }
+
+    fun getTmpVar() : String {
+        return _tmpVar(tmpVarIndex ++)
+    }
+
+    fun generate() = builder().build().toString()
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/ColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/ColumnTypeAdapter.kt
new file mode 100644
index 0000000..31ab2c9
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/ColumnTypeAdapter.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.squareup.javapoet.TypeName
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A code generator that can read a field from Cursor and write a field to a Statement
+ */
+abstract class ColumnTypeAdapter(val out: TypeMirror) {
+    val outTypeName by lazy { TypeName.get(out) }
+    abstract fun readFromCursor(outVarName : String, cursorVarName: String, index: Int,
+                                scope: CodeGenScope)
+
+    abstract fun bindToStmt(stmtName: String, index: Int, valueVarName: String,
+                            scope: CodeGenScope)
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/CompositeAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/CompositeAdapter.kt
new file mode 100644
index 0000000..5d52668
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/CompositeAdapter.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.android.support.room.ext.L
+import com.android.support.room.ext.T
+import com.squareup.javapoet.TypeName
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A column adapter that uses multiple type adapters to do the conversion.
+ */
+class CompositeAdapter(out: TypeMirror, val columnTypeAdapter: ColumnTypeAdapter,
+                       val typeConverters: List<TypeConverter>) : ColumnTypeAdapter(out) {
+    override fun readFromCursor(outVarName: String, cursorVarName: String, index: Int,
+                                scope: CodeGenScope) {
+        val reversed = typeConverters.reversed()
+
+        scope.builder().apply {
+            val tmpCursorValue = scope.getTmpVar()
+            addStatement("final $T $L", columnTypeAdapter.outTypeName, tmpCursorValue)
+            columnTypeAdapter.readFromCursor(tmpCursorValue, cursorVarName, index, scope)
+            var tmpInVar = tmpCursorValue
+            var tmpOutVar = scope.getTmpVar()
+            reversed.take(reversed.size - 1).forEach {
+                addStatement("final $T $L", it.fromTypeName, tmpOutVar)
+                it.convertBackward(tmpInVar, tmpOutVar, scope)
+                tmpInVar = tmpOutVar
+                tmpOutVar = scope.getTmpVar()
+            }
+            reversed.last().convertBackward(tmpInVar, outVarName, scope)
+        }
+    }
+
+    override fun bindToStmt(stmtName: String, index: Int, valueVarName: String,
+                            scope: CodeGenScope) {
+        var tmpInVar = valueVarName
+        var tmpOutVar = scope.getTmpVar()
+        scope.builder().apply {
+            typeConverters.forEach {
+                addStatement("final $T $L", it.toTypeName, tmpOutVar)
+                it.convertForward(tmpInVar, tmpOutVar, scope)
+                tmpInVar = tmpOutVar
+                tmpOutVar = scope.getTmpVar()
+            }
+            columnTypeAdapter.bindToStmt(stmtName, index, tmpInVar, scope)
+        }
+    }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/IntListConverter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/IntListConverter.kt
new file mode 100644
index 0000000..093661e
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/IntListConverter.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.android.support.room.ext.L
+import com.android.support.room.ext.N
+import com.android.support.room.ext.T
+import com.squareup.javapoet.ClassName
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeMirror
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+class IntListConverter(first : TypeMirror, second : TypeMirror) : TypeConverter(first, second) {
+    companion object {
+        fun create(processingEnv : ProcessingEnvironment) : IntListConverter {
+            val stringType = processingEnv.elementUtils
+                    .getTypeElement(String::class.java.canonicalName)
+                    .asType()
+            val intType = processingEnv.elementUtils
+                    .getTypeElement(Integer::class.java.canonicalName)
+                    .asType()
+            val listType = processingEnv.elementUtils
+                    .getTypeElement(java.util.List::class.java.canonicalName)
+            val listOfInts = processingEnv.typeUtils.getDeclaredType(listType, intType)
+            return IntListConverter(stringType, listOfInts)
+        }
+
+        val STRING_UTIL: ClassName = ClassName.get("com.android.support.room.util", "StringUtil")
+    }
+    override fun convertForward(inputVarName: String, outputVarName: String,
+                                scope: CodeGenScope) {
+        scope.builder()
+                .addStatement("$L = $T.splitToIntList($L)", outputVarName, STRING_UTIL,
+                        inputVarName)
+    }
+
+    override fun convertBackward(inputVarName: String, outputVarName: String,
+                                 scope: CodeGenScope) {
+        scope.builder()
+                .addStatement("$L = $T.joinIntoString($L)", outputVarName, STRING_UTIL,
+                        inputVarName)
+    }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/PrimitiveBooleanToIntConverter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/PrimitiveBooleanToIntConverter.kt
new file mode 100644
index 0000000..f4f312b
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/PrimitiveBooleanToIntConverter.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.android.support.room.ext.L
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeKind
+
+/**
+ * int to boolean adapter.
+ */
+class PrimitiveBooleanToIntConverter(processingEnvironment: ProcessingEnvironment) : TypeConverter(
+        from = processingEnvironment.typeUtils.getPrimitiveType(TypeKind.BOOLEAN),
+        to = processingEnvironment.typeUtils.getPrimitiveType(TypeKind.INT)) {
+    override fun convertForward(inputVarName: String, outputVarName: String,
+                                scope: CodeGenScope) {
+        scope.builder().addStatement("$L = $L ? 1 : 0", outputVarName, inputVarName)
+    }
+
+    override fun convertBackward(inputVarName: String, outputVarName: String,
+                                 scope: CodeGenScope) {
+        scope.builder().addStatement("$L = $L != 0", outputVarName, inputVarName)
+    }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/PrimitiveColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/PrimitiveColumnTypeAdapter.kt
new file mode 100644
index 0000000..13178ce
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/PrimitiveColumnTypeAdapter.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.android.support.room.ext.L
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.PrimitiveType
+import javax.lang.model.type.TypeKind
+
+/**
+ * Adapters for all primitives that has direct cursor mappings.
+ */
+open class PrimitiveColumnTypeAdapter(out: PrimitiveType,
+                                      val cursorGetter: String,
+                                      val stmtSetter: String,
+                                      cast: Boolean = false) : ColumnTypeAdapter(out) {
+    val cast = if (cast) "(${out.kind.name.toLowerCase()}) " else ""
+
+    companion object {
+        fun createPrimitiveAdapters(processingEnvironment: ProcessingEnvironment)
+                : List<ColumnTypeAdapter> {
+            return listOf(PrimitiveColumnTypeAdapter(
+                    out = processingEnvironment.typeUtils.getPrimitiveType(TypeKind.INT),
+                    cursorGetter = "getInt",
+                    stmtSetter = "bindLong"),
+                    PrimitiveColumnTypeAdapter(
+                            out = processingEnvironment.typeUtils.getPrimitiveType(TypeKind.SHORT),
+                            cursorGetter = "getShort",
+                            stmtSetter = "bindLong"),
+                    PrimitiveColumnTypeAdapter(
+                            out = processingEnvironment.typeUtils.getPrimitiveType(TypeKind.BYTE),
+                            cursorGetter = "getShort",
+                            stmtSetter = "bindLong",
+                            cast = true),
+                    PrimitiveColumnTypeAdapter(
+                            out = processingEnvironment.typeUtils.getPrimitiveType(TypeKind.LONG),
+                            cursorGetter = "getLong",
+                            stmtSetter = "bindLong"),
+                    PrimitiveColumnTypeAdapter(
+                            out = processingEnvironment.typeUtils.getPrimitiveType(TypeKind.CHAR),
+                            cursorGetter = "getInt",
+                            stmtSetter = "bindLong",
+                            cast = true),
+                    PrimitiveColumnTypeAdapter(
+                            out = processingEnvironment.typeUtils.getPrimitiveType(TypeKind.FLOAT),
+                            cursorGetter = "getFloat",
+                            stmtSetter = "bindDouble"),
+                    PrimitiveColumnTypeAdapter(
+                            out = processingEnvironment.typeUtils.getPrimitiveType(TypeKind.DOUBLE),
+                            cursorGetter = "getDouble",
+                            stmtSetter = "bindDouble"))
+        }
+    }
+
+    override fun bindToStmt(stmtName: String, index: Int, valueVarName: String,
+                            scope: CodeGenScope) {
+        scope.builder()
+                .addStatement("$L.$L($L, $L)", stmtName, stmtSetter, index, valueVarName)
+    }
+
+    override fun readFromCursor(outVarName: String, cursorVarName: String, index: Int,
+                                scope: CodeGenScope) {
+        scope.builder()
+                .addStatement("$L = $L$L.$L($L)", outVarName, cast, cursorVarName,
+                        cursorGetter, index)
+    }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/ReverseTypeConverter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/ReverseTypeConverter.kt
new file mode 100644
index 0000000..919a374
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/ReverseTypeConverter.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+/**
+ * Takes a type adapter and reverses it.
+ */
+class ReverseTypeConverter(val delegate: TypeConverter) : TypeConverter(delegate.to,
+        delegate.from) {
+    override fun convertForward(inputVarName: String, outputVarName: String,
+                                scope: CodeGenScope) {
+        return delegate.convertBackward(inputVarName, outputVarName, scope)
+    }
+
+    override fun convertBackward(inputVarName: String, outputVarName: String,
+                                 scope: CodeGenScope) {
+        return delegate.convertForward(inputVarName, outputVarName, scope)
+    }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/StringColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/StringColumnTypeAdapter.kt
new file mode 100644
index 0000000..a6ee779
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/StringColumnTypeAdapter.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.android.support.room.ext.L
+import javax.annotation.processing.ProcessingEnvironment
+
+class StringColumnTypeAdapter(processingEnvironment: ProcessingEnvironment)
+    : ColumnTypeAdapter((processingEnvironment.elementUtils.getTypeElement(
+        String::class.java.canonicalName)).asType()) {
+    override fun readFromCursor(outVarName: String, cursorVarName: String, index: Int,
+                                scope: CodeGenScope) {
+        scope.builder()
+                .addStatement("$L = $L.isNull($L) ? null : $L.getString($L)",
+                        outVarName, cursorVarName, index, cursorVarName, index)
+    }
+
+    override fun bindToStmt(stmtName: String, index: Int, valueVarName: String,
+                            scope: CodeGenScope) {
+        scope.builder().apply {
+            beginControlFlow("if ($L == null)", valueVarName)
+                    .addStatement("$L.bindNull($L)", stmtName, index)
+            nextControlFlow("else")
+                    .addStatement("$L.bindString($L, $L)", stmtName, index, valueVarName)
+            endControlFlow()
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
new file mode 100644
index 0000000..2e1d5de
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Holds all type adapters and can create on demand composite type adapters to convert a type into a
+ * database column.
+ */
+class TypeAdapterStore(val roundEnv: RoundEnvironment,
+                       val processingEnvironment: ProcessingEnvironment) {
+    private val columnTypeAdapters = arrayListOf<ColumnTypeAdapter>()
+    private val typeAdapters = arrayListOf<TypeConverter>()
+
+    init {
+        PrimitiveColumnTypeAdapter.createPrimitiveAdapters(processingEnvironment).forEach {
+            addColumnAdapter(it)
+        }
+        addColumnAdapter(StringColumnTypeAdapter(processingEnvironment))
+        addTypeAdapter(IntListConverter.create(processingEnvironment))
+        addTypeAdapter(PrimitiveBooleanToIntConverter(processingEnvironment))
+    }
+
+    fun getTypeAdapters(input : TypeMirror, excludes : Set<TypeMirror>) : List<TypeConverter>? {
+        // TODO optimize
+        return if (findColumnAdapters(input).isNotEmpty()) {
+            emptyList()
+        } else {
+            val candidate = findTypeAdapters(input)
+                    .filterNot { excludes.contains(it.to) }
+                    .map {
+                        Pair(it, getTypeAdapters(it.to, excludes + it.from))
+                    }
+                    .filterNot { it.second == null }
+                    .sortedBy { it.second!!.size }
+                    .firstOrNull()
+            return if (candidate == null) {
+                null
+            } else {
+                listOf(candidate.first) + candidate.second!!
+            }
+        }
+    }
+
+    fun getAdapterFor(out : TypeMirror) : ColumnTypeAdapter? {
+        val adapters = findColumnAdapters(out)
+        if (adapters.isNotEmpty()) {
+            return adapters.last()
+        }
+        val typeAdapters = getTypeAdapters(out, setOf(out))
+        return if (typeAdapters == null) {
+            null
+        } else {
+            return CompositeAdapter(out, findColumnAdapters(typeAdapters.last().to).last(),
+                    typeAdapters)
+        }
+    }
+
+    fun addTypeAdapter(converter: TypeConverter) {
+        typeAdapters.add(converter)
+        typeAdapters.add(ReverseTypeConverter(converter))
+    }
+
+    fun addColumnAdapter(adapter: ColumnTypeAdapter) {
+        columnTypeAdapters.add(adapter)
+    }
+
+    fun findColumnAdapters(input : TypeMirror) : List<ColumnTypeAdapter> {
+        return columnTypeAdapters.filter {
+            processingEnvironment.typeUtils.isSameType(input, it.out)
+        }
+    }
+
+    fun findTypeAdapters(input : TypeMirror) : List<TypeConverter> {
+        return typeAdapters.filter {
+            processingEnvironment.typeUtils.isSameType(input, it.from)
+        }
+    }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeConverter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeConverter.kt
new file mode 100644
index 0000000..68d5f79
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeConverter.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.squareup.javapoet.TypeName
+import javax.lang.model.type.TypeMirror
+
+/**
+ * A code generator that can convert between two types
+ */
+abstract class TypeConverter(val from: TypeMirror, val to: TypeMirror) {
+    val fromTypeName: TypeName by lazy { TypeName.get(from) }
+    val toTypeName: TypeName by lazy { TypeName.get(to) }
+    abstract fun convertForward(inputVarName: String, outputVarName: String,
+                                scope: CodeGenScope)
+
+    abstract fun convertBackward(inputVarName: String, outputVarName: String,
+                                 scope: CodeGenScope)
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/EntityProcessorTestTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/EntityProcessorTest.kt
similarity index 98%
rename from room/compiler/src/test/kotlin/com/android/support/room/processor/EntityProcessorTestTest.kt
rename to room/compiler/src/test/kotlin/com/android/support/room/processor/EntityProcessorTest.kt
index d80a2d3..f1add6b 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/EntityProcessorTestTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/EntityProcessorTest.kt
@@ -28,7 +28,7 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class EntityProcessorTestTest : BaseEntityParserTest() {
+class EntityProcessorTest : BaseEntityParserTest() {
     @Test
     fun simple() {
         singleEntity("""
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/solver/BasicColumnTypeAdaptersTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/solver/BasicColumnTypeAdaptersTest.kt
new file mode 100644
index 0000000..3c9a937
--- /dev/null
+++ b/room/compiler/src/test/kotlin/com/android/support/room/solver/BasicColumnTypeAdaptersTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.android.support.room.testing.TestInvocation
+import com.squareup.javapoet.*
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import simpleRun
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+
+@RunWith(Parameterized::class)
+class BasicColumnTypeAdaptersTest(val input: Input, val bindCode: String,
+                                  val cursorCode: String) {
+    val scope = CodeGenScope()
+
+    companion object {
+        val SQLITE_STMT : TypeName = ClassName.get("android.database.sqlite", "SQLiteStatement")
+        val CURSOR: TypeName = ClassName.get("android.database", "Cursor")
+
+        @Parameterized.Parameters(name = "kind:{0},bind:_{1},cursor:_{2}")
+        @JvmStatic
+        fun params(): List<Array<Any>> {
+            return listOf(
+                    arrayOf(Input(TypeKind.INT),
+                            "st.bindLong(6, inp);",
+                            "out = crs.getInt(9);"),
+                    arrayOf(Input(TypeKind.BYTE),
+                            "st.bindLong(6, inp);",
+                            "out = (byte) crs.getShort(9);"),
+                    arrayOf(Input(TypeKind.SHORT),
+                            "st.bindLong(6, inp);",
+                            "out = crs.getShort(9);"),
+                    arrayOf(Input(TypeKind.LONG),
+                            "st.bindLong(6, inp);",
+                            "out = crs.getLong(9);"),
+                    arrayOf(Input(TypeKind.CHAR),
+                            "st.bindLong(6, inp);",
+                            "out = (char) crs.getInt(9);"),
+                    arrayOf(Input(TypeKind.FLOAT),
+                            "st.bindDouble(6, inp);",
+                            "out = crs.getFloat(9);"),
+                    arrayOf(Input(TypeKind.DOUBLE),
+                            "st.bindDouble(6, inp);",
+                            "out = crs.getDouble(9);"),
+                    arrayOf(Input(TypeKind.DECLARED, "java.lang.String"),
+                            """
+                            if (inp == null) {
+                              st.bindNull(6);
+                            } else {
+                              st.bindString(6, inp);
+                            }
+                            """.trimIndent(),
+                            "out = crs.isNull(9) ? null : crs.getString(9);")
+            )
+        }
+    }
+
+    @Test
+    fun bind() {
+        simpleRun { invocation ->
+            val adapter = TypeAdapterStore(invocation.roundEnv, invocation.processingEnv)
+                    .getAdapterFor(input.getTypeMirror(invocation.processingEnv))!!
+            adapter.bindToStmt("st", 6, "inp", scope)
+            assertThat(scope.generate().trim(), `is`(bindCode))
+            generateCode(invocation)
+        }.compilesWithoutError()
+    }
+
+    private fun generateCode(invocation: TestInvocation) {
+        val typeMirror = input.getTypeMirror(invocation.processingEnv)
+        val spec = TypeSpec.classBuilder("OutClass")
+                .addField(FieldSpec.builder(SQLITE_STMT, "st").build())
+                .addField(FieldSpec.builder(CURSOR, "crs").build())
+                .addField(FieldSpec.builder(TypeName.get(typeMirror), "out").build())
+                .addField(FieldSpec.builder(TypeName.get(typeMirror), "inp").build())
+                .addMethod(
+                        MethodSpec.methodBuilder("foo")
+                                .addCode(scope.builder().build())
+                                .build()
+                )
+                .build()
+        JavaFile.builder("foo.bar", spec).build().writeTo(invocation.processingEnv.filer)
+    }
+
+    @Test
+    fun read() {
+        simpleRun { invocation ->
+            val adapter = TypeAdapterStore(invocation.roundEnv, invocation.processingEnv)
+                    .getAdapterFor(input.getTypeMirror(invocation.processingEnv))!!
+            adapter.readFromCursor("out", "crs", 9, scope)
+            assertThat(scope.generate().trim(), `is`(cursorCode))
+            generateCode(invocation)
+        }.compilesWithoutError()
+    }
+
+    data class Input(val typeKind: TypeKind, val qName : String? = null) {
+        fun getTypeMirror(processingEnv: ProcessingEnvironment) : TypeMirror {
+            return if (typeKind.isPrimitive) {
+                processingEnv.typeUtils.getPrimitiveType(typeKind)
+            } else {
+                processingEnv.elementUtils.getTypeElement(qName).asType()
+            }
+        }
+    }
+}
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/solver/TypeAdapterStoreTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/solver/TypeAdapterStoreTest.kt
new file mode 100644
index 0000000..25fce2f
--- /dev/null
+++ b/room/compiler/src/test/kotlin/com/android/support/room/solver/TypeAdapterStoreTest.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.solver
+
+import com.android.support.room.Entity
+import com.android.support.room.ext.L
+import com.android.support.room.ext.T
+import com.android.support.room.testing.TestInvocation
+import com.android.support.room.testing.TestProcessor
+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.*
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.type.TypeKind
+
+@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+@RunWith(JUnit4::class)
+class TypeAdapterStoreTest {
+    companion object {
+        fun tmp(index: Int) = CodeGenScope._tmpVar(index)
+    }
+
+    @Test
+    fun testDirect() {
+        singleRun { invocation ->
+            val store = TypeAdapterStore(invocation.roundEnv, invocation.processingEnv)
+            val primitiveType = invocation.processingEnv.typeUtils.getPrimitiveType(TypeKind.INT)
+            val adapter = store.getAdapterFor(primitiveType)
+            assertThat(adapter, notNullValue())
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun testVia1TypeAdapter() {
+        singleRun { invocation ->
+            val store = TypeAdapterStore(invocation.roundEnv, invocation.processingEnv)
+            val booleanType = invocation.processingEnv.typeUtils
+                    .getPrimitiveType(TypeKind.BOOLEAN)
+            val adapter = store.getAdapterFor(booleanType)
+            assertThat(adapter, notNullValue())
+            assertThat(adapter, instanceOf(CompositeAdapter::class.java))
+            val bindScope = CodeGenScope()
+            adapter!!.bindToStmt("stmt", 41, "fooVar", bindScope)
+            assertThat(bindScope.generate().trim(), `is`("""
+                    final int ${tmp(0)};
+                    ${tmp(0)} = fooVar ? 1 : 0;
+                    stmt.bindLong(41, ${tmp(0)});
+                    """.trimIndent()))
+
+            val cursorScope = CodeGenScope()
+            adapter.readFromCursor("res", "curs", 7, cursorScope)
+            assertThat(cursorScope.generate().trim(), `is`("""
+                    final int ${tmp(0)};
+                    ${tmp(0)} = curs.getInt(7);
+                    res = ${tmp(0)} != 0;
+                    """.trimIndent()))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun testVia2TypeAdapters() {
+        singleRun { invocation ->
+            val store = TypeAdapterStore(invocation.roundEnv, invocation.processingEnv)
+            store.addTypeAdapter(PointTypeConverter(invocation.processingEnv))
+            val pointType = invocation.processingEnv.elementUtils
+                    .getTypeElement("foo.bar.Point").asType()
+            val adapter = store.getAdapterFor(pointType)
+            assertThat(adapter, notNullValue())
+            assertThat(adapter, instanceOf(CompositeAdapter::class.java))
+
+            val bindScope = CodeGenScope()
+            adapter!!.bindToStmt("stmt", 41, "fooVar", bindScope)
+            assertThat(bindScope.generate().trim(), `is`("""
+                    final boolean ${tmp(0)};
+                    ${tmp(0)} = foo.bar.Point.toBoolean(fooVar);
+                    final int ${tmp(1)};
+                    ${tmp(1)} = ${tmp(0)} ? 1 : 0;
+                    stmt.bindLong(41, ${tmp(1)});
+                    """.trimIndent()))
+
+            val cursorScope = CodeGenScope()
+            adapter.readFromCursor("res", "curs", 11, cursorScope).toString()
+            assertThat(cursorScope.generate().trim(), `is`("""
+                    final int ${tmp(0)};
+                    ${tmp(0)} = curs.getInt(11);
+                    final boolean ${tmp(1)};
+                    ${tmp(1)} = ${tmp(0)} != 0;
+                    res = foo.bar.Point.fromBoolean(${tmp(1)});
+                    """.trimIndent()))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun testIntList() {
+        singleRun { invocation ->
+            val store = TypeAdapterStore(invocation.roundEnv, invocation.processingEnv)
+            val intType = invocation.processingEnv.elementUtils
+                    .getTypeElement(Integer::class.java.canonicalName)
+                    .asType()
+            val listType = invocation.processingEnv.elementUtils
+                    .getTypeElement(java.util.List::class.java.canonicalName)
+            val listOfInts = invocation.processingEnv.typeUtils.getDeclaredType(listType, intType)
+            val adapter = store.getAdapterFor(listOfInts)
+            assertThat(adapter, notNullValue())
+
+            val bindScope = CodeGenScope()
+            adapter!!.bindToStmt("stmt", 41, "fooVar", bindScope)
+            assertThat(bindScope.generate().trim(), `is`("""
+                    final java.lang.String ${tmp(0)};
+                    ${tmp(0)} = com.android.support.room.util.StringUtil.joinIntoString(fooVar);
+                    if (${tmp(0)} == null) {
+                      stmt.bindNull(41);
+                    } else {
+                      stmt.bindString(41, ${tmp(0)});
+                    }
+                    """.trimIndent()))
+        }.compilesWithoutError()
+    }
+
+    fun singleRun(handler: (TestInvocation) -> Unit): CompileTester {
+        return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+                .that(listOf(JavaFileObjects.forSourceString("foo.bar.DummyClass",
+                        """
+                        package foo.bar;
+                        import com.android.support.room.*;
+                        @Entity
+                        public class DummyClass {}
+                        """
+                ), JavaFileObjects.forSourceString("foo.bar.Point",
+                        """
+                        package foo.bar;
+                        import com.android.support.room.*;
+                        @Entity
+                        public class Point {
+                            public int x, y;
+                            public Point(int x, int y) {
+                                this.x = x;
+                                this.y = y;
+                            }
+                            public static Point fromBoolean(boolean val) {
+                                return val ? new Point(1, 1) : new Point(0, 0);
+                            }
+                            public static boolean toBoolean(Point point) {
+                                return point.x > 0;
+                            }
+                        }
+                        """
+                )))
+                .processedWith(TestProcessor.builder()
+                        .forAnnotations(Entity::class)
+                        .nextRunHandler { invocation ->
+                            handler(invocation)
+                            true
+                        }
+                        .build())
+    }
+
+    class PointTypeConverter(processingEnv: ProcessingEnvironment) : TypeConverter(
+            from = processingEnv.elementUtils.getTypeElement("foo.bar.Point").asType(),
+            to = processingEnv.typeUtils.getPrimitiveType(TypeKind.BOOLEAN)) {
+        override fun convertForward(inputVarName: String, outputVarName: String,
+                                    scope: CodeGenScope) {
+            scope.builder().apply {
+                addStatement("$L = $T.toBoolean($L)", outputVarName, from, inputVarName)
+            }
+        }
+
+        override fun convertBackward(inputVarName: String, outputVarName: String,
+                                     scope: CodeGenScope) {
+            scope.builder().apply {
+                addStatement("$L = $T.fromBoolean($L)", outputVarName, from, inputVarName)
+            }
+        }
+    }
+}
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/testing/InProcessorTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/testing/InProcessorTest.kt
index 79386d1..ff9629f 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/testing/InProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/testing/InProcessorTest.kt
@@ -34,11 +34,13 @@
         val didRun = AtomicBoolean(false)
         Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
                 .that(JavaFileObjects.forSourceString("foo.bar.MyClass",
-                        "package foo.bar;" +
-                                "abstract public class MyClass {" +
-                                "  @com.android.support.room.Query(\"foo\")" +
-                                "  abstract public java.util.List<Object> getUsers();" +
-                                "}"))
+                        """
+                        package foo.bar;
+                        abstract public class MyClass {
+                        @com.android.support.room.Query("foo")
+                        abstract public void setFoo(String foo);
+                        }
+                        """))
                 .processedWith(TestProcessor.builder()
                         .nextRunHandler { invocation ->
                             didRun.set(true)
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt b/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt
index 6496e7a..7e04f96 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/testing/TestProcessor.kt
@@ -18,7 +18,6 @@
 
 import com.android.support.room.errors.ElementBoundException
 import javax.annotation.processing.AbstractProcessor
-import javax.annotation.processing.ProcessingEnvironment
 import javax.annotation.processing.RoundEnvironment
 import javax.annotation.processing.SupportedSourceVersion
 import javax.lang.model.SourceVersion
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/testing/test_util.kt b/room/compiler/src/test/kotlin/com/android/support/room/testing/test_util.kt
new file mode 100644
index 0000000..fe71691
--- /dev/null
+++ b/room/compiler/src/test/kotlin/com/android/support/room/testing/test_util.kt
@@ -0,0 +1,43 @@
+import com.android.support.room.Query
+import com.android.support.room.testing.TestInvocation
+import com.android.support.room.testing.TestProcessor
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourceSubjectFactory
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert
+
+/*
+ * Copyright (C) 2016 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.
+ */
+fun simpleRun(f: (TestInvocation) -> Unit): CompileTester {
+    return Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
+            .that(JavaFileObjects.forSourceString("foo.bar.MyClass",
+                    """
+                    package foo.bar;
+                    abstract public class MyClass {
+                    @com.android.support.room.Query("foo")
+                    abstract public void setFoo(String foo);
+                    }
+                    """))
+            .processedWith(TestProcessor.builder()
+                    .nextRunHandler {
+                        f(it)
+                        true
+                    }
+                    .forAnnotations(Query::class)
+                    .build())
+}
diff --git a/room/runtime/build.gradle b/room/runtime/build.gradle
new file mode 100644
index 0000000..b5c72f8
--- /dev/null
+++ b/room/runtime/build.gradle
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion compile_sdk_version
+    buildToolsVersion build_tools_version
+
+    defaultConfig {
+        minSdkVersion min_sdk_version
+        targetSdkVersion target_sdk_version
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+
+    testOptions {
+        unitTests.returnDefaultValues = true
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+}
+
+dependencies {
+    compile project(":common")
+    compile "com.android.support:support-core-utils:$support_lib_version"
+
+    testCompile "junit:junit:$junit_version"
+    testCompile "org.mockito:mockito-core:$mockito_version"
+}
+
+createAndroidCheckstyle(project)
diff --git a/room/runtime/src/main/AndroidManifest.xml b/room/runtime/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..40fdf10
--- /dev/null
+++ b/room/runtime/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright (C) 2016 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.support.room">
+</manifest>
diff --git a/room/runtime/src/main/java/com/android/support/room/util/StringUtil.java b/room/runtime/src/main/java/com/android/support/room/util/StringUtil.java
new file mode 100644
index 0000000..f48f492
--- /dev/null
+++ b/room/runtime/src/main/java/com/android/support/room/util/StringUtil.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.util;
+
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * String utilities for Room
+ */
+@SuppressWarnings("WeakerAccess")
+public class StringUtil {
+
+    /**
+     * Splits a comma separated list of integers to integer list.
+     * <p>
+     * If an input is malformed, it is omitted from the result.
+     *
+     * @param input Comma separated list of integers.
+     * @return A List containing the integers or null if the input is null.
+     */
+    @Nullable
+    static List<Integer> splitToIntList(@Nullable String input) {
+        if (input == null) {
+            return null;
+        }
+        List<Integer> result = new ArrayList<>();
+        StringTokenizer tokenizer = new StringTokenizer(input, ",");
+        while (tokenizer.hasMoreElements()) {
+            final String item = tokenizer.nextToken();
+            try {
+                result.add(Integer.parseInt(item));
+            } catch (NumberFormatException ex) {
+                Log.e("ROOM", "Malformed integer list", ex);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Joins the given list of integers into a comma separated list.
+     *
+     * @param input The list of integers.
+     * @return Comma separated string composed of integers in the list. If the list is null, return
+     * value is null.
+     */
+    @Nullable
+    static String joinIntoString(@Nullable List<Integer> input) {
+        if (input == null) {
+            return null;
+        }
+
+        final int size = input.size();
+        if (size == 0) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < size; i++) {
+            sb.append(Integer.toString(input.get(i)));
+            if (i < size - 1) {
+                sb.append(",");
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git a/room/runtime/src/test/java/com/android/support/room/util/StringUtilTest.java b/room/runtime/src/test/java/com/android/support/room/util/StringUtilTest.java
new file mode 100644
index 0000000..f4a5d81
--- /dev/null
+++ b/room/runtime/src/test/java/com/android/support/room/util/StringUtilTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.util;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Arrays.asList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collections;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(JUnit4.class)
+public class StringUtilTest {
+    @Test
+    public void testEmpty() {
+        assertThat(StringUtil.splitToIntList(""), is(Collections.<Integer>emptyList()));
+        assertThat(StringUtil.joinIntoString(Collections.<Integer>emptyList()), is(""));
+    }
+
+    @Test
+    public void testNull() {
+        assertThat(StringUtil.splitToIntList(null), nullValue());
+        assertThat(StringUtil.joinIntoString(null), nullValue());
+    }
+
+    @Test
+    public void testSingle() {
+        assertThat(StringUtil.splitToIntList("4"), is(asList(4)));
+        assertThat(StringUtil.joinIntoString(asList(4)), is("4"));
+    }
+
+    @Test
+    public void testMultiple() {
+        assertThat(StringUtil.splitToIntList("4,5"), is(asList(4, 5)));
+        assertThat(StringUtil.joinIntoString(asList(4, 5)), is("4,5"));
+    }
+
+    @Test
+    public void testNegative() {
+        assertThat(StringUtil.splitToIntList("-4,-5,6,-7"), is(asList(-4, -5, 6, -7)));
+        assertThat(StringUtil.joinIntoString(asList(-4, -5, 6, -7)), is("-4,-5,6,-7"));
+    }
+
+    @Test
+    public void ignoreMalformed() {
+        assertThat(StringUtil.splitToIntList("-4,a,5,7"), is(asList(-4, 5, 7)));
+    }
+}
diff --git a/room/settings.gradle b/room/settings.gradle
index db66435..19185a1 100644
--- a/room/settings.gradle
+++ b/room/settings.gradle
@@ -13,6 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 include 'common'
+include 'runtime'
 include 'compiler'