Merge "Add XML persistence code generation tool."

This commit is contained in:
TreeHugger Robot
2020-09-25 21:11:55 +00:00
committed by Android (Google) Code Review
9 changed files with 995 additions and 0 deletions

View 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();
}

View File

@@ -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();
}

View File

@@ -0,0 +1,11 @@
java_binary_host {
name: "xmlpersistence_cli",
manifest: "manifest.txt",
srcs: [
"src/**/*.kt",
],
static_libs: [
"javaparser-symbol-solver",
"javapoet",
],
}

View File

@@ -0,0 +1 @@
zhanghai@google.com

View File

@@ -0,0 +1 @@
Main-class: MainKt

View 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
}

View 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>")
}

View 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())
}

View 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) }