blob: b2c5f4ac767b624df9f517ef99b74ea5677ed907 [file] [log] [blame]
/*
* 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 Generated code
// @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 (\$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
}