Add XML persistence code generation tool.

Test: m xmlpersistence_cli && xmlpersistence_cli
Change-Id: Ia54739e99f52c3ab7125f21b1bc306a474f6bf68
diff --git a/tools/xmlpersistence/Android.bp b/tools/xmlpersistence/Android.bp
new file mode 100644
index 0000000..d58d0dc
--- /dev/null
+++ b/tools/xmlpersistence/Android.bp
@@ -0,0 +1,11 @@
+java_binary_host {
+    name: "xmlpersistence_cli",
+    manifest: "manifest.txt",
+    srcs: [
+        "src/**/*.kt",
+    ],
+    static_libs: [
+        "javaparser-symbol-solver",
+        "javapoet",
+    ],
+}
diff --git a/tools/xmlpersistence/OWNERS b/tools/xmlpersistence/OWNERS
new file mode 100644
index 0000000..4f4d06a
--- /dev/null
+++ b/tools/xmlpersistence/OWNERS
@@ -0,0 +1 @@
+zhanghai@google.com
diff --git a/tools/xmlpersistence/manifest.txt b/tools/xmlpersistence/manifest.txt
new file mode 100644
index 0000000..6d97719
--- /dev/null
+++ b/tools/xmlpersistence/manifest.txt
@@ -0,0 +1 @@
+Main-class: MainKt
diff --git a/tools/xmlpersistence/src/main/kotlin/Generator.kt b/tools/xmlpersistence/src/main/kotlin/Generator.kt
new file mode 100644
index 0000000..28467b7
--- /dev/null
+++ b/tools/xmlpersistence/src/main/kotlin/Generator.kt
@@ -0,0 +1,578 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.NameAllocator
+import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.TypeSpec
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+import java.time.Year
+import java.util.Objects
+import javax.lang.model.element.Modifier
+
+// JavaPoet only supports line comments, and can't add a newline after file level comments.
+val FILE_HEADER = """
+    /*
+     * Copyright (C) ${Year.now().value} 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.
+     */
+
+    // Generated by xmlpersistence. DO NOT MODIFY!
+    // CHECKSTYLE:OFF
+    // @formatter:off
+""".trimIndent() + "\n\n"
+
+private val atomicFileType = ClassName.get("android.util", "AtomicFile")
+
+fun generate(persistence: PersistenceInfo): JavaFile {
+    val distinctClassFields = persistence.root.allClassFields.distinctBy { it.type }
+    val type = TypeSpec.classBuilder(persistence.name)
+        .addJavadoc(
+            """
+                Generated class implementing XML persistence for${'$'}W{@link $1T}.
+                <p>
+                This class provides atomicity for persistence via {@link $2T}, however it does not provide
+                thread safety, so please bring your own synchronization mechanism.
+            """.trimIndent(), persistence.root.type, atomicFileType
+        )
+        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+        .addField(generateFileField())
+        .addMethod(generateConstructor())
+        .addMethod(generateReadMethod(persistence.root))
+        .addMethod(generateParseMethod(persistence.root))
+        .addMethods(distinctClassFields.map { generateParseClassMethod(it) })
+        .addMethod(generateWriteMethod(persistence.root))
+        .addMethod(generateSerializeMethod(persistence.root))
+        .addMethods(distinctClassFields.map { generateSerializeClassMethod(it) })
+        .addMethod(generateDeleteMethod())
+        .build()
+    return JavaFile.builder(persistence.root.type.packageName(), type)
+        .skipJavaLangImports(true)
+        .indent("    ")
+        .build()
+}
+
+private val nonNullType = ClassName.get("android.annotation", "NonNull")
+
+private fun generateFileField(): FieldSpec =
+    FieldSpec.builder(atomicFileType, "mFile", Modifier.PRIVATE, Modifier.FINAL)
+        .addAnnotation(nonNullType)
+        .build()
+
+private fun generateConstructor(): MethodSpec =
+    MethodSpec.constructorBuilder()
+        .addJavadoc(
+            """
+                Create an instance of this class.
+
+                @param file the XML file for persistence
+            """.trimIndent()
+        )
+        .addModifiers(Modifier.PUBLIC)
+        .addParameter(
+            ParameterSpec.builder(File::class.java, "file").addAnnotation(nonNullType).build()
+        )
+        .addStatement("mFile = new \$1T(file)", atomicFileType)
+        .build()
+
+private val nullableType = ClassName.get("android.annotation", "Nullable")
+
+private val xmlPullParserType = ClassName.get("org.xmlpull.v1", "XmlPullParser")
+
+private val xmlType = ClassName.get("android.util", "Xml")
+
+private val xmlPullParserExceptionType = ClassName.get("org.xmlpull.v1", "XmlPullParserException")
+
+private fun generateReadMethod(rootField: ClassFieldInfo): MethodSpec =
+    MethodSpec.methodBuilder("read")
+        .addJavadoc(
+            """
+                Read${'$'}W{@link $1T}${'$'}Wfrom${'$'}Wthe${'$'}WXML${'$'}Wfile.
+
+                @return the persisted${'$'}W{@link $1T},${'$'}Wor${'$'}W{@code null}${'$'}Wif${'$'}Wthe${'$'}WXML${'$'}Wfile${'$'}Wdoesn't${'$'}Wexist
+                @throws IllegalArgumentException if an error occurred while reading
+            """.trimIndent(), rootField.type
+        )
+        .addAnnotation(nullableType)
+        .addModifiers(Modifier.PUBLIC)
+        .returns(rootField.type)
+        .addControlFlow(
+            "try (final \$1T inputStream = mFile.openRead())", FileInputStream::class.java
+        ) {
+            addStatement("final \$1T parser = \$2T.newPullParser()", xmlPullParserType, xmlType)
+            addStatement("parser.setInput(inputStream, null)")
+            addStatement("return parse(parser)")
+            nextControlFlow("catch (\$1T e)", FileNotFoundException::class.java)
+            addStatement("return null")
+            nextControlFlow(
+                "catch (\$1T | \$2T e)", IOException::class.java, xmlPullParserExceptionType
+            )
+            addStatement("throw new IllegalArgumentException(e)")
+        }
+        .build()
+
+private val ClassFieldInfo.allClassFields: List<ClassFieldInfo>
+    get() =
+        mutableListOf<ClassFieldInfo>().apply {
+            this += this@allClassFields
+            for (field in fields) {
+                when (field) {
+                    is ClassFieldInfo -> this += field.allClassFields
+                    is ListFieldInfo -> this += field.element.allClassFields
+                }
+            }
+        }
+
+private fun generateParseMethod(rootField: ClassFieldInfo): MethodSpec =
+    MethodSpec.methodBuilder("parse")
+        .addAnnotation(nonNullType)
+        .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
+        .returns(rootField.type)
+        .addParameter(
+            ParameterSpec.builder(xmlPullParserType, "parser").addAnnotation(nonNullType).build()
+        )
+        .addExceptions(listOf(ClassName.get(IOException::class.java), xmlPullParserExceptionType))
+        .apply {
+            addStatement("int type")
+            addStatement("int depth")
+            addStatement("int innerDepth = parser.getDepth() + 1")
+            addControlFlow(
+                "while ((type = parser.next()) != \$1T.END_DOCUMENT\$W"
+                    + "&& ((depth = parser.getDepth()) >= innerDepth || type != \$1T.END_TAG))",
+                xmlPullParserType
+            ) {
+                addControlFlow(
+                    "if (depth > innerDepth || type != \$1T.START_TAG)", xmlPullParserType
+                ) {
+                    addStatement("continue")
+                }
+                addControlFlow(
+                    "if (\$1T.equals(parser.getName(),\$W\$2S))", Objects::class.java,
+                    rootField.tagName
+                ) {
+                    addStatement("return \$1L(parser)", rootField.parseMethodName)
+                }
+            }
+            addStatement(
+                "throw new IllegalArgumentException(\$1S)",
+                "Missing root tag <${rootField.tagName}>"
+            )
+        }
+        .build()
+
+private fun generateParseClassMethod(classField: ClassFieldInfo): MethodSpec =
+    MethodSpec.methodBuilder(classField.parseMethodName)
+        .addAnnotation(nonNullType)
+        .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
+        .returns(classField.type)
+        .addParameter(
+            ParameterSpec.builder(xmlPullParserType, "parser").addAnnotation(nonNullType).build()
+        )
+        .apply {
+            val (attributeFields, tagFields) = classField.fields
+                .partition { it is PrimitiveFieldInfo || it is StringFieldInfo }
+            if (tagFields.isNotEmpty()) {
+                addExceptions(
+                    listOf(ClassName.get(IOException::class.java), xmlPullParserExceptionType)
+                )
+            }
+            val nameAllocator = NameAllocator().apply {
+                newName("parser")
+                newName("type")
+                newName("depth")
+                newName("innerDepth")
+            }
+            for (field in attributeFields) {
+                val variableName = nameAllocator.newName(field.variableName, field)
+                when (field) {
+                    is PrimitiveFieldInfo -> {
+                        val stringVariableName =
+                            nameAllocator.newName("${field.variableName}String")
+                        addStatement(
+                            "final String \$1L =\$Wparser.getAttributeValue(null,\$W\$2S)",
+                            stringVariableName, field.attributeName
+                        )
+                        if (field.isRequired) {
+                            addControlFlow("if (\$1L == null)", stringVariableName) {
+                                addStatement(
+                                    "throw new IllegalArgumentException(\$1S)",
+                                    "Missing attribute \"${field.attributeName}\""
+                                )
+                            }
+                        }
+                        val boxedType = field.type.box()
+                        val parseTypeMethodName = if (field.type.isPrimitive) {
+                            "parse${field.type.toString().capitalize()}"
+                        } else {
+                            "valueOf"
+                        }
+                        if (field.isRequired) {
+                            addStatement(
+                                "final \$1T \$2L =\$W\$3T.\$4L($5L)", field.type, variableName,
+                                boxedType, parseTypeMethodName, stringVariableName
+                            )
+                        } else {
+                            addStatement(
+                                "final \$1T \$2L =\$W$3L != null ?\$W\$4T.\$5L($3L)\$W: null",
+                                field.type, variableName, stringVariableName, boxedType,
+                                parseTypeMethodName
+                            )
+                        }
+                    }
+                    is StringFieldInfo ->
+                        addStatement(
+                            "final String \$1L =\$Wparser.getAttributeValue(null,\$W\$2S)",
+                            variableName, field.attributeName
+                        )
+                    else -> error(field)
+                }
+            }
+            if (tagFields.isNotEmpty()) {
+                for (field in tagFields) {
+                    val variableName = nameAllocator.newName(field.variableName, field)
+                    when (field) {
+                        is ClassFieldInfo ->
+                            addStatement("\$1T \$2L =\$Wnull", field.type, variableName)
+                        is ListFieldInfo ->
+                            addStatement(
+                                "final \$1T \$2L =\$Wnew \$3T<>()", field.type, variableName,
+                                ArrayList::class.java
+                            )
+                        else -> error(field)
+                    }
+                }
+                addStatement("int type")
+                addStatement("int depth")
+                addStatement("int innerDepth = parser.getDepth() + 1")
+                addControlFlow(
+                    "while ((type = parser.next()) != \$1T.END_DOCUMENT\$W"
+                        + "&& ((depth = parser.getDepth()) >= innerDepth || type != \$1T.END_TAG))",
+                    xmlPullParserType
+                ) {
+                    addControlFlow(
+                        "if (depth > innerDepth || type != \$1T.START_TAG)", xmlPullParserType
+                    ) {
+                        addStatement("continue")
+                    }
+                    addControlFlow("switch (parser.getName())") {
+                        for (field in tagFields) {
+                            addControlFlow("case \$1S:", field.tagName) {
+                                val variableName = nameAllocator.get(field)
+                                when (field) {
+                                    is ClassFieldInfo -> {
+                                        addControlFlow("if (\$1L != null)", variableName) {
+                                            addStatement(
+                                                "throw new IllegalArgumentException(\$1S)",
+                                                "Duplicate tag \"${field.tagName}\""
+                                            )
+                                        }
+                                        addStatement(
+                                            "\$1L =\$W\$2L(parser)", variableName,
+                                            field.parseMethodName
+                                        )
+                                        addStatement("break")
+                                    }
+                                    is ListFieldInfo -> {
+                                        val elementNameAllocator = nameAllocator.clone()
+                                        val elementVariableName = elementNameAllocator.newName(
+                                            field.element.xmlName!!.toLowerCamelCase()
+                                        )
+                                        addStatement(
+                                            "final \$1T \$2L =\$W\$3L(parser)", field.element.type,
+                                            elementVariableName, field.element.parseMethodName
+                                        )
+                                        addStatement(
+                                            "\$1L.add(\$2L)", variableName, elementVariableName
+                                        )
+                                        addStatement("break")
+                                    }
+                                    else -> error(field)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            for (field in tagFields.filter { it is ClassFieldInfo && it.isRequired }) {
+                addControlFlow("if ($1L == null)", nameAllocator.get(field)) {
+                    addStatement(
+                        "throw new IllegalArgumentException(\$1S)", "Missing tag <${field.tagName}>"
+                    )
+                }
+            }
+            addStatement(
+                classField.fields.joinToString(",\$W", "return new \$1T(", ")") {
+                    nameAllocator.get(it)
+                }, classField.type
+            )
+        }
+        .build()
+
+private val ClassFieldInfo.parseMethodName: String
+    get() = "parse${type.simpleName().toUpperCamelCase()}"
+
+private val xmlSerializerType = ClassName.get("org.xmlpull.v1", "XmlSerializer")
+
+private fun generateWriteMethod(rootField: ClassFieldInfo): MethodSpec =
+    MethodSpec.methodBuilder("write")
+        .apply {
+            val nameAllocator = NameAllocator().apply {
+                newName("outputStream")
+                newName("serializer")
+            }
+            val parameterName = nameAllocator.newName(rootField.variableName)
+            addJavadoc(
+                """
+                    Write${'$'}W{@link $1T}${'$'}Wto${'$'}Wthe${'$'}WXML${'$'}Wfile.
+
+                    @param $2L the${'$'}W{@link ${'$'}1T}${'$'}Wto${'$'}Wpersist
+                """.trimIndent(), rootField.type, parameterName
+            )
+            addAnnotation(nullableType)
+            addModifiers(Modifier.PUBLIC)
+            addParameter(
+                ParameterSpec.builder(rootField.type, parameterName)
+                    .addAnnotation(nonNullType)
+                    .build()
+            )
+            addStatement("\$1T outputStream = null", FileOutputStream::class.java)
+            addControlFlow("try") {
+                addStatement("outputStream = mFile.startWrite()")
+                addStatement(
+                    "final \$1T serializer =\$W\$2T.newSerializer()", xmlSerializerType, xmlType
+                )
+                addStatement(
+                    "serializer.setOutput(outputStream, \$1T.UTF_8.name())",
+                    StandardCharsets::class.java
+                )
+                addStatement(
+                    "serializer.setFeature(\$1S, true)",
+                    "http://xmlpull.org/v1/doc/features.html#indent-output"
+                )
+                addStatement("serializer.startDocument(null, true)")
+                addStatement("serialize(serializer,\$W\$1L)", parameterName)
+                addStatement("serializer.endDocument()")
+                addStatement("mFile.finishWrite(outputStream)")
+                nextControlFlow("catch (Exception e)")
+                addStatement("e.printStackTrace()")
+                addStatement("mFile.failWrite(outputStream)")
+            }
+        }
+        .build()
+
+private fun generateSerializeMethod(rootField: ClassFieldInfo): MethodSpec =
+    MethodSpec.methodBuilder("serialize")
+        .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
+        .addParameter(
+            ParameterSpec.builder(xmlSerializerType, "serializer")
+                .addAnnotation(nonNullType)
+                .build()
+        )
+        .apply {
+            val nameAllocator = NameAllocator().apply { newName("serializer") }
+            val parameterName = nameAllocator.newName(rootField.variableName)
+            addParameter(
+                ParameterSpec.builder(rootField.type, parameterName)
+                    .addAnnotation(nonNullType)
+                    .build()
+            )
+            addException(IOException::class.java)
+            addStatement("serializer.startTag(null, \$1S)", rootField.tagName)
+            addStatement("\$1L(serializer, \$2L)", rootField.serializeMethodName, parameterName)
+            addStatement("serializer.endTag(null, \$1S)", rootField.tagName)
+        }
+        .build()
+
+private fun generateSerializeClassMethod(classField: ClassFieldInfo): MethodSpec =
+    MethodSpec.methodBuilder(classField.serializeMethodName)
+        .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
+        .addParameter(
+            ParameterSpec.builder(xmlSerializerType, "serializer")
+                .addAnnotation(nonNullType)
+                .build()
+        )
+        .apply {
+            val nameAllocator = NameAllocator().apply {
+                newName("serializer")
+                newName("i")
+            }
+            val parameterName = nameAllocator.newName(classField.serializeParameterName)
+            addParameter(
+                ParameterSpec.builder(classField.type, parameterName)
+                    .addAnnotation(nonNullType)
+                    .build()
+            )
+            addException(IOException::class.java)
+            val (attributeFields, tagFields) = classField.fields
+                .partition { it is PrimitiveFieldInfo || it is StringFieldInfo }
+            for (field in attributeFields) {
+                val variableName = "$parameterName.${field.name}"
+                if (!field.isRequired) {
+                    beginControlFlow("if (\$1L != null)", variableName)
+                }
+                when (field) {
+                    is PrimitiveFieldInfo -> {
+                        if (field.isRequired && !field.type.isPrimitive) {
+                            addControlFlow("if (\$1L == null)", variableName) {
+                                addStatement(
+                                    "throw new IllegalArgumentException(\$1S)",
+                                    "Field \"${field.name}\" is null"
+                                )
+                            }
+                        }
+                        val stringVariableName =
+                            nameAllocator.newName("${field.variableName}String")
+                        addStatement(
+                            "final String \$1L =\$WString.valueOf(\$2L)", stringVariableName,
+                            variableName
+                        )
+                        addStatement(
+                            "serializer.attribute(null, \$1S, \$2L)", field.attributeName,
+                            stringVariableName
+                        )
+                    }
+                    is StringFieldInfo -> {
+                        if (field.isRequired) {
+                            addControlFlow("if (\$1L == null)", variableName) {
+                                addStatement(
+                                    "throw new IllegalArgumentException(\$1S)",
+                                    "Field \"${field.name}\" is null"
+                                )
+                            }
+                        }
+                        addStatement(
+                            "serializer.attribute(null, \$1S, \$2L)", field.attributeName,
+                            variableName
+                        )
+                    }
+                    else -> error(field)
+                }
+                if (!field.isRequired) {
+                    endControlFlow()
+                }
+            }
+            for (field in tagFields) {
+                val variableName = "$parameterName.${field.name}"
+                if (field.isRequired) {
+                    addControlFlow("if (\$1L == null)", variableName) {
+                        addStatement(
+                            "throw new IllegalArgumentException(\$1S)",
+                            "Field \"${field.name}\" is null"
+                        )
+                    }
+                }
+                when (field) {
+                    is ClassFieldInfo -> {
+                        addStatement("serializer.startTag(null, \$1S)", field.tagName)
+                        addStatement(
+                            "\$1L(serializer, \$2L)", field.serializeMethodName, variableName
+                        )
+                        addStatement("serializer.endTag(null, \$1S)", field.tagName)
+                    }
+                    is ListFieldInfo -> {
+                        val sizeVariableName = nameAllocator.newName("${field.variableName}Size")
+                        addStatement(
+                            "final int \$1L =\$W\$2L.size()", sizeVariableName, variableName
+                        )
+                        addControlFlow("for (int i = 0;\$Wi < \$1L;\$Wi++)", sizeVariableName) {
+                            val elementNameAllocator = nameAllocator.clone()
+                            val elementVariableName = elementNameAllocator.newName(
+                                field.element.xmlName!!.toLowerCamelCase()
+                            )
+                            addStatement(
+                                "final \$1T \$2L =\$W\$3L.get(i)", field.element.type,
+                                elementVariableName, variableName
+                            )
+                            addControlFlow("if (\$1L == null)", elementVariableName) {
+                                addStatement(
+                                    "throw new IllegalArgumentException(\$1S\$W+ i\$W+ \$2S)",
+                                    "Field element \"${field.name}[", "]\" is null"
+                                )
+                            }
+                            addStatement("serializer.startTag(null, \$1S)", field.element.tagName)
+                            addStatement(
+                                "\$1L(serializer,\$W\$2L)", field.element.serializeMethodName,
+                                elementVariableName
+                            )
+                            addStatement("serializer.endTag(null, \$1S)", field.element.tagName)
+                        }
+                    }
+                    else -> error(field)
+                }
+            }
+        }
+        .build()
+
+private val ClassFieldInfo.serializeMethodName: String
+    get() = "serialize${type.simpleName().toUpperCamelCase()}"
+
+private val ClassFieldInfo.serializeParameterName: String
+    get() = type.simpleName().toLowerCamelCase()
+
+private val FieldInfo.variableName: String
+    get() = name.toLowerCamelCase()
+
+private val FieldInfo.attributeName: String
+    get() {
+        check(this is PrimitiveFieldInfo || this is StringFieldInfo)
+        return xmlNameOrName.toLowerCamelCase()
+    }
+
+private val FieldInfo.tagName: String
+    get() {
+        check(this is ClassFieldInfo || this is ListFieldInfo)
+        return xmlNameOrName.toLowerKebabCase()
+    }
+
+private val FieldInfo.xmlNameOrName: String
+    get() = xmlName ?: name
+
+private fun generateDeleteMethod(): MethodSpec =
+    MethodSpec.methodBuilder("delete")
+        .addJavadoc("Delete the XML file, if any.")
+        .addModifiers(Modifier.PUBLIC)
+        .addStatement("mFile.delete()")
+        .build()
+
+private inline fun MethodSpec.Builder.addControlFlow(
+    controlFlow: String,
+    vararg args: Any,
+    block: MethodSpec.Builder.() -> Unit
+): MethodSpec.Builder {
+    beginControlFlow(controlFlow, *args)
+    block()
+    endControlFlow()
+    return this
+}
diff --git a/tools/xmlpersistence/src/main/kotlin/Main.kt b/tools/xmlpersistence/src/main/kotlin/Main.kt
new file mode 100644
index 0000000..e271f8c
--- /dev/null
+++ b/tools/xmlpersistence/src/main/kotlin/Main.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+import java.io.File
+import java.nio.file.Files
+
+fun main(args: Array<String>) {
+    val showUsage = args.isEmpty() || when (args.singleOrNull()) {
+        "-h", "--help" -> true
+        else -> false
+    }
+    if (showUsage) {
+        usage()
+        return
+    }
+
+    val files = args.flatMap {
+        File(it).walk().filter { it.isFile && it.extension == "java" }.map { it.toPath() }
+    }
+    val persistences = parse(files)
+    for (persistence in persistences) {
+        val file = generate(persistence)
+        Files.newBufferedWriter(persistence.path).use {
+            it.write(FILE_HEADER)
+            file.writeTo(it)
+        }
+    }
+}
+
+private fun usage() {
+    println("Usage: xmlpersistence <FILES>")
+}
diff --git a/tools/xmlpersistence/src/main/kotlin/Parser.kt b/tools/xmlpersistence/src/main/kotlin/Parser.kt
new file mode 100644
index 0000000..3ea12a9
--- /dev/null
+++ b/tools/xmlpersistence/src/main/kotlin/Parser.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+import com.github.javaparser.JavaParser
+import com.github.javaparser.ParseProblemException
+import com.github.javaparser.ParseResult
+import com.github.javaparser.ParserConfiguration
+import com.github.javaparser.ast.Node
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.FieldDeclaration
+import com.github.javaparser.ast.body.TypeDeclaration
+import com.github.javaparser.ast.expr.AnnotationExpr
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.NormalAnnotationExpr
+import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr
+import com.github.javaparser.ast.expr.StringLiteralExpr
+import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration
+import com.github.javaparser.resolution.types.ResolvedPrimitiveType
+import com.github.javaparser.resolution.types.ResolvedReferenceType
+import com.github.javaparser.symbolsolver.JavaSymbolSolver
+import com.github.javaparser.symbolsolver.javaparsermodel.declarations.JavaParserClassDeclaration
+import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver
+import com.github.javaparser.symbolsolver.resolution.typesolvers.MemoryTypeSolver
+import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import java.nio.file.Path
+import java.util.Optional
+
+class PersistenceInfo(
+    val name: String,
+    val root: ClassFieldInfo,
+    val path: Path
+)
+
+sealed class FieldInfo {
+    abstract val name: String
+    abstract val xmlName: String?
+    abstract val type: TypeName
+    abstract val isRequired: Boolean
+}
+
+class PrimitiveFieldInfo(
+    override val name: String,
+    override val xmlName: String?,
+    override val type: TypeName,
+    override val isRequired: Boolean
+) : FieldInfo()
+
+class StringFieldInfo(
+    override val name: String,
+    override val xmlName: String?,
+    override val isRequired: Boolean
+) : FieldInfo() {
+    override val type: TypeName = ClassName.get(String::class.java)
+}
+
+class ClassFieldInfo(
+    override val name: String,
+    override val xmlName: String?,
+    override val type: ClassName,
+    override val isRequired: Boolean,
+    val fields: List<FieldInfo>
+) : FieldInfo()
+
+class ListFieldInfo(
+    override val name: String,
+    override val xmlName: String?,
+    override val type: ParameterizedTypeName,
+    val element: ClassFieldInfo
+) : FieldInfo() {
+    override val isRequired: Boolean = true
+}
+
+fun parse(files: List<Path>): List<PersistenceInfo> {
+    val typeSolver = CombinedTypeSolver().apply { add(ReflectionTypeSolver()) }
+    val javaParser = JavaParser(ParserConfiguration()
+        .setSymbolResolver(JavaSymbolSolver(typeSolver)))
+    val compilationUnits = files.map { javaParser.parse(it).getOrThrow() }
+    val memoryTypeSolver = MemoryTypeSolver().apply {
+        for (compilationUnit in compilationUnits) {
+            for (typeDeclaration in compilationUnit.getNodesByClass<TypeDeclaration<*>>()) {
+                val name = typeDeclaration.fullyQualifiedName.getOrNull() ?: continue
+                addDeclaration(name, typeDeclaration.resolve())
+            }
+        }
+    }
+    typeSolver.add(memoryTypeSolver)
+    return mutableListOf<PersistenceInfo>().apply {
+        for (compilationUnit in compilationUnits) {
+            val classDeclarations = compilationUnit
+                .getNodesByClass<ClassOrInterfaceDeclaration>()
+                .filter { !it.isInterface && (!it.isNestedType || it.isStatic) }
+            this += classDeclarations.mapNotNull { parsePersistenceInfo(it) }
+        }
+    }
+}
+
+private fun parsePersistenceInfo(classDeclaration: ClassOrInterfaceDeclaration): PersistenceInfo? {
+    val annotation = classDeclaration.getAnnotationByName("XmlPersistence").getOrNull()
+        ?: return null
+    val rootClassName = classDeclaration.nameAsString
+    val name = annotation.getMemberValue("value")?.stringLiteralValue
+        ?: "${rootClassName}Persistence"
+    val rootXmlName = classDeclaration.getAnnotationByName("XmlName").getOrNull()
+        ?.getMemberValue("value")?.stringLiteralValue
+    val root = parseClassFieldInfo(
+        rootXmlName ?: rootClassName, rootXmlName, true, classDeclaration
+    )
+    val path = classDeclaration.findCompilationUnit().get().storage.get().path
+        .resolveSibling("$name.java")
+    return PersistenceInfo(name, root, path)
+}
+
+private fun parseClassFieldInfo(
+    name: String,
+    xmlName: String?,
+    isRequired: Boolean,
+    classDeclaration: ClassOrInterfaceDeclaration
+): ClassFieldInfo {
+    val fields = classDeclaration.fields.filterNot { it.isStatic }.map { parseFieldInfo(it) }
+    val type = classDeclaration.resolve().typeName
+    return ClassFieldInfo(name, xmlName, type, isRequired, fields)
+}
+
+private fun parseFieldInfo(field: FieldDeclaration): FieldInfo {
+    require(field.isPublic && field.isFinal)
+    val variable = field.variables.single()
+    val name = variable.nameAsString
+    val annotations = field.annotations + variable.type.annotations
+    val annotation = annotations.getByName("XmlName")
+    val xmlName = annotation?.getMemberValue("value")?.stringLiteralValue
+    val isRequired = annotations.getByName("NonNull") != null
+    return when (val type = variable.type.resolve()) {
+        is ResolvedPrimitiveType -> {
+            val primitiveType = type.typeName
+            PrimitiveFieldInfo(name, xmlName, primitiveType, true)
+        }
+        is ResolvedReferenceType -> {
+            when (type.qualifiedName) {
+                Boolean::class.javaObjectType.name, Byte::class.javaObjectType.name,
+                Short::class.javaObjectType.name, Char::class.javaObjectType.name,
+                Integer::class.javaObjectType.name, Long::class.javaObjectType.name,
+                Float::class.javaObjectType.name, Double::class.javaObjectType.name ->
+                    PrimitiveFieldInfo(name, xmlName, type.typeName, isRequired)
+                String::class.java.name -> StringFieldInfo(name, xmlName, isRequired)
+                List::class.java.name -> {
+                    requireNotNull(xmlName)
+                    val elementType = type.typeParametersValues().single()
+                    require(elementType is ResolvedReferenceType)
+                    val listType = ParameterizedTypeName.get(
+                        ClassName.get(List::class.java), elementType.typeName
+                    )
+                    val element = parseClassFieldInfo(
+                        "(element)", xmlName, true, elementType.classDeclaration
+                    )
+                    ListFieldInfo(name, xmlName, listType, element)
+                }
+                else -> parseClassFieldInfo(name, xmlName, isRequired, type.classDeclaration)
+            }
+        }
+        else -> error(type)
+    }
+}
+
+private fun <T> ParseResult<T>.getOrThrow(): T =
+    if (isSuccessful) {
+        result.get()
+    } else {
+        throw ParseProblemException(problems)
+    }
+
+private inline fun <reified T : Node> Node.getNodesByClass(): List<T> =
+    getNodesByClass(T::class.java)
+
+private fun <T : Node> Node.getNodesByClass(klass: Class<T>): List<T> = mutableListOf<T>().apply {
+    if (klass.isInstance(this@getNodesByClass)) {
+        this += klass.cast(this@getNodesByClass)
+    }
+    for (childNode in childNodes) {
+        this += childNode.getNodesByClass(klass)
+    }
+}
+
+private fun <T> Optional<T>.getOrNull(): T? = orElse(null)
+
+private fun List<AnnotationExpr>.getByName(name: String): AnnotationExpr? =
+    find { it.name.identifier == name }
+
+private fun AnnotationExpr.getMemberValue(name: String): Expression? =
+    when (this) {
+        is NormalAnnotationExpr -> pairs.find { it.nameAsString == name }?.value
+        is SingleMemberAnnotationExpr -> if (name == "value") memberValue else null
+        else -> null
+    }
+
+private val Expression.stringLiteralValue: String
+    get() {
+        require(this is StringLiteralExpr)
+        return value
+    }
+
+private val ResolvedReferenceType.classDeclaration: ClassOrInterfaceDeclaration
+    get() {
+        val resolvedClassDeclaration = typeDeclaration
+        require(resolvedClassDeclaration is JavaParserClassDeclaration)
+        return resolvedClassDeclaration.wrappedNode
+    }
+
+private val ResolvedPrimitiveType.typeName: TypeName
+    get() =
+        when (this) {
+            ResolvedPrimitiveType.BOOLEAN -> TypeName.BOOLEAN
+            ResolvedPrimitiveType.BYTE -> TypeName.BYTE
+            ResolvedPrimitiveType.SHORT -> TypeName.SHORT
+            ResolvedPrimitiveType.CHAR -> TypeName.CHAR
+            ResolvedPrimitiveType.INT -> TypeName.INT
+            ResolvedPrimitiveType.LONG -> TypeName.LONG
+            ResolvedPrimitiveType.FLOAT -> TypeName.FLOAT
+            ResolvedPrimitiveType.DOUBLE -> TypeName.DOUBLE
+        }
+
+// This doesn't support type parameters.
+private val ResolvedReferenceType.typeName: TypeName
+    get() = typeDeclaration.typeName
+
+private val ResolvedReferenceTypeDeclaration.typeName: ClassName
+    get() {
+        val packageName = packageName
+        val classNames = className.split(".")
+        val topLevelClassName = classNames.first()
+        val nestedClassNames = classNames.drop(1)
+        return ClassName.get(packageName, topLevelClassName, *nestedClassNames.toTypedArray())
+    }
diff --git a/tools/xmlpersistence/src/main/kotlin/StringCaseExtensions.kt b/tools/xmlpersistence/src/main/kotlin/StringCaseExtensions.kt
new file mode 100644
index 0000000..b4bdbba
--- /dev/null
+++ b/tools/xmlpersistence/src/main/kotlin/StringCaseExtensions.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+import java.util.Locale
+
+private val camelHumpBoundary = Regex(
+    "-"
+    + "|_"
+    + "|(?<=[0-9])(?=[^0-9])"
+    + "|(?<=[A-Z])(?=[^A-Za-z]|[A-Z][a-z])"
+    + "|(?<=[a-z])(?=[^a-z])"
+)
+
+private fun String.toCamelHumps(): List<String> = split(camelHumpBoundary)
+
+fun String.toUpperCamelCase(): String =
+    toCamelHumps().joinToString("") { it.toLowerCase(Locale.ROOT).capitalize(Locale.ROOT) }
+
+fun String.toLowerCamelCase(): String = toUpperCamelCase().decapitalize(Locale.ROOT)
+
+fun String.toUpperKebabCase(): String =
+    toCamelHumps().joinToString("-") { it.toUpperCase(Locale.ROOT) }
+
+fun String.toLowerKebabCase(): String =
+    toCamelHumps().joinToString("-") { it.toLowerCase(Locale.ROOT) }
+
+fun String.toUpperSnakeCase(): String =
+    toCamelHumps().joinToString("_") { it.toUpperCase(Locale.ROOT) }
+
+fun String.toLowerSnakeCase(): String =
+    toCamelHumps().joinToString("_") { it.toLowerCase(Locale.ROOT) }