Merge "Add XML persistence code generation tool."
This commit is contained in:
committed by
Android (Google) Code Review
commit
b77466b184
31
services/core/java/com/android/server/utils/XmlName.java
Normal file
31
services/core/java/com/android/server/utils/XmlName.java
Normal file
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
11
tools/xmlpersistence/Android.bp
Normal file
11
tools/xmlpersistence/Android.bp
Normal file
@@ -0,0 +1,11 @@
|
||||
java_binary_host {
|
||||
name: "xmlpersistence_cli",
|
||||
manifest: "manifest.txt",
|
||||
srcs: [
|
||||
"src/**/*.kt",
|
||||
],
|
||||
static_libs: [
|
||||
"javaparser-symbol-solver",
|
||||
"javapoet",
|
||||
],
|
||||
}
|
||||
1
tools/xmlpersistence/OWNERS
Normal file
1
tools/xmlpersistence/OWNERS
Normal file
@@ -0,0 +1 @@
|
||||
zhanghai@google.com
|
||||
1
tools/xmlpersistence/manifest.txt
Normal file
1
tools/xmlpersistence/manifest.txt
Normal file
@@ -0,0 +1 @@
|
||||
Main-class: MainKt
|
||||
578
tools/xmlpersistence/src/main/kotlin/Generator.kt
Normal file
578
tools/xmlpersistence/src/main/kotlin/Generator.kt
Normal file
@@ -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
|
||||
}
|
||||
45
tools/xmlpersistence/src/main/kotlin/Main.kt
Normal file
45
tools/xmlpersistence/src/main/kotlin/Main.kt
Normal file
@@ -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>")
|
||||
}
|
||||
248
tools/xmlpersistence/src/main/kotlin/Parser.kt
Normal file
248
tools/xmlpersistence/src/main/kotlin/Parser.kt
Normal file
@@ -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())
|
||||
}
|
||||
44
tools/xmlpersistence/src/main/kotlin/StringCaseExtensions.kt
Normal file
44
tools/xmlpersistence/src/main/kotlin/StringCaseExtensions.kt
Normal file
@@ -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) }
|
||||
Reference in New Issue
Block a user