/* * 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}.

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 get() = mutableListOf().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 }