diff options
author | 2020-09-24 12:18:37 -0700 | |
---|---|---|
committer | 2020-09-24 18:15:29 -0700 | |
commit | 9c37f9ca704c8622a0e3878236390b1ac29c52d4 (patch) | |
tree | a784b3729ee9a513c514437e249ac461675257b1 | |
parent | 7cdbbc1decd90a6788711a076308d7bb303f3a6e (diff) |
Add XML persistence code generation tool.
Test: m xmlpersistence_cli && xmlpersistence_cli
Change-Id: Ia54739e99f52c3ab7125f21b1bc306a474f6bf68
-rw-r--r-- | services/core/java/com/android/server/utils/XmlName.java | 31 | ||||
-rw-r--r-- | services/core/java/com/android/server/utils/XmlPersistence.java | 36 | ||||
-rw-r--r-- | tools/xmlpersistence/Android.bp | 11 | ||||
-rw-r--r-- | tools/xmlpersistence/OWNERS | 1 | ||||
-rw-r--r-- | tools/xmlpersistence/manifest.txt | 1 | ||||
-rw-r--r-- | tools/xmlpersistence/src/main/kotlin/Generator.kt | 578 | ||||
-rw-r--r-- | tools/xmlpersistence/src/main/kotlin/Main.kt | 45 | ||||
-rw-r--r-- | tools/xmlpersistence/src/main/kotlin/Parser.kt | 248 | ||||
-rw-r--r-- | tools/xmlpersistence/src/main/kotlin/StringCaseExtensions.kt | 44 |
9 files changed, 995 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/utils/XmlName.java b/services/core/java/com/android/server/utils/XmlName.java new file mode 100644 index 000000000000..c0e22daaa32e --- /dev/null +++ b/services/core/java/com/android/server/utils/XmlName.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +package com.android.server.utils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify the XML name for the annotated field or persistence class. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.FIELD, ElementType.TYPE }) +public @interface XmlName { + String value(); +} diff --git a/services/core/java/com/android/server/utils/XmlPersistence.java b/services/core/java/com/android/server/utils/XmlPersistence.java new file mode 100644 index 000000000000..8900a9d4783e --- /dev/null +++ b/services/core/java/com/android/server/utils/XmlPersistence.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package com.android.server.utils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Generate XML persistence for the annotated class. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface XmlPersistence { + /** + * Name for the generated XML persistence class. + * <p> + * The name defaults to the target class name appended with {@code Persistence} if unspecified. + */ + String value(); +} diff --git a/tools/xmlpersistence/Android.bp b/tools/xmlpersistence/Android.bp new file mode 100644 index 000000000000..d58d0dcdc45a --- /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 000000000000..4f4d06a32676 --- /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 000000000000..6d9771998efc --- /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 000000000000..28467b7fc0b0 --- /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 000000000000..e271f8cb9361 --- /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 000000000000..3ea12a9aa389 --- /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 000000000000..b4bdbba7170b --- /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) } |