This commit is contained in:
Benoit Marty 2023-06-23 17:58:51 +02:00
parent cd292488b6
commit ff09ba1208
10 changed files with 395 additions and 3 deletions

View File

@ -239,8 +239,6 @@ ext.groups = [
regex: [
],
group: [
// https://github.com/cmelchior/realmfieldnameshelper/issues/42
'dk.ilios',
'im.dlg',
'me.dm7.barcodescanner',
'me.gujun.android',

View File

@ -0,0 +1,23 @@
apply plugin: 'kotlin'
apply plugin: 'java'
sourceCompatibility = '1.7'
targetCompatibility = '1.7'
dependencies {
implementation 'com.squareup:javapoet:1.13.0'
}
task javadocJar(type: Jar, dependsOn: 'javadoc') {
from javadoc.destinationDir
classifier = 'javadoc'
}
task sourcesJar(type: Jar, dependsOn: 'classes') {
from sourceSets.main.allSource
classifier = 'sources'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}

View File

@ -0,0 +1,24 @@
package dk.ilios.realmfieldnames
import java.util.TreeMap
/**
* Class responsible for keeping track of the metadata for each Realm model class.
*/
class ClassData(val packageName: String?, val simpleClassName: String, val libraryClass: Boolean = false) {
val fields = TreeMap<String, String?>() // <fieldName, linkedType or null>
fun addField(field: String, linkedType: String?) {
fields.put(field, linkedType)
}
val qualifiedClassName: String
get() {
if (packageName != null && !packageName.isEmpty()) {
return packageName + "." + simpleClassName
} else {
return simpleClassName
}
}
}

View File

@ -0,0 +1,75 @@
package dk.ilios.realmfieldnames
import java.util.Locale
/**
* Class for encapsulating the rules for converting between the field name in the Realm model class
* and the matching name in the "&lt;class&gt;Fields" class.
*/
class FieldNameFormatter {
@JvmOverloads fun format(fieldName: String?, locale: Locale = Locale.US): String {
if (fieldName == null || fieldName == "") {
return ""
}
// Normalize word separator chars
val normalizedFieldName : String = fieldName.replace('-', '_')
// Iterate field name using the following rules
// lowerCase m followed by upperCase anything is considered hungarian notation
// lowercase char followed by uppercase char is considered camel case
// Two uppercase chars following each other is considered non-standard camelcase
// _ and - are treated as word separators
val result = StringBuilder(normalizedFieldName.length)
if (normalizedFieldName.codePointCount(0, normalizedFieldName.length) == 1) {
result.append(normalizedFieldName)
} else {
var previousCodepoint: Int?
var currentCodepoint: Int? = null
val length = normalizedFieldName.length
var offset = 0
while (offset < length) {
previousCodepoint = currentCodepoint
currentCodepoint = normalizedFieldName.codePointAt(offset)
if (previousCodepoint != null) {
if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && previousCodepoint === 'm'.code as Int? && result.length == 1) {
// Hungarian notation starting with: mX
result.delete(0, 1)
result.appendCodePoint(currentCodepoint)
} else if (Character.isUpperCase(currentCodepoint) && Character.isUpperCase(previousCodepoint)) {
// InvalidCamelCase: XXYx (should have been xxYx)
if (offset + Character.charCount(currentCodepoint) < normalizedFieldName.length) {
val nextCodePoint = normalizedFieldName.codePointAt(offset + Character.charCount(currentCodepoint))
if (Character.isLowerCase(nextCodePoint)) {
result.append("_")
}
}
result.appendCodePoint(currentCodepoint)
} else if (currentCodepoint === '-'.code as Int? || currentCodepoint === '_'.code as Int?) {
// Word-separator: x-x or x_x
result.append("_")
} else if (Character.isUpperCase(currentCodepoint) && !Character.isUpperCase(previousCodepoint) && Character.isLetterOrDigit(previousCodepoint)) {
// camelCase: xX
result.append("_")
result.appendCodePoint(currentCodepoint)
} else {
// Unknown type
result.appendCodePoint(currentCodepoint)
}
} else {
// Only triggered for first code point
result.appendCodePoint(currentCodepoint)
}
offset += Character.charCount(currentCodepoint)
}
}
return result.toString().uppercase(locale)
}
}

View File

@ -0,0 +1,82 @@
package dk.ilios.realmfieldnames
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.TypeSpec
import java.io.IOException
import javax.annotation.processing.Filer
import javax.lang.model.element.Modifier
/**
* Class responsible for creating the final output files.
*/
class FileGenerator(private val filer: Filer) {
private val formatter: FieldNameFormatter
init {
this.formatter = FieldNameFormatter()
}
/**
* Generates all the "&lt;class&gt;Fields" fields with field name references.
* @param fileData Files to create.
* *
* @return `true` if the files where generated, `false` if not.
*/
fun generate(fileData: Set<ClassData>): Boolean {
return fileData
.filter { !it.libraryClass }
.all { generateFile(it, fileData) }
}
private fun generateFile(classData: ClassData, classPool: Set<ClassData>): Boolean {
val fileBuilder = TypeSpec.classBuilder(classData.simpleClassName + "Fields")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addJavadoc("This class enumerate all queryable fields in {@link \$L.\$L}\n",
classData.packageName, classData.simpleClassName)
// Add a static field reference to each queryable field in the Realm model class
classData.fields.forEach { fieldName, value ->
if (value != null) {
// Add linked field names (only up to depth 1)
for (data in classPool) {
if (data.qualifiedClassName == value) {
val linkedTypeSpec = TypeSpec.classBuilder(formatter.format(fieldName))
.addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)
val linkedClassFields = data.fields
addField(linkedTypeSpec, "$", fieldName)
for (linkedFieldName in linkedClassFields.keys) {
addField(linkedTypeSpec, linkedFieldName, fieldName + "." + linkedFieldName)
}
fileBuilder.addType(linkedTypeSpec.build())
}
}
} else {
// Add normal field name
addField(fileBuilder, fieldName, fieldName)
}
}
val javaFile = JavaFile.builder(classData.packageName, fileBuilder.build()).build()
try {
javaFile.writeTo(filer)
return true
} catch (e: IOException) {
e.printStackTrace()
return false
}
}
private fun addField(fileBuilder: TypeSpec.Builder, fieldName: String, fieldNameValue: String) {
val field = FieldSpec.builder(String::class.java, formatter.format(fieldName))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer("\$S", fieldNameValue)
.build()
fileBuilder.addField(field)
}
}

View File

@ -0,0 +1,187 @@
package dk.ilios.realmfieldnames
import java.util.*
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Messager
import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.RoundEnvironment
import javax.annotation.processing.SupportedAnnotationTypes
import javax.lang.model.SourceVersion
import javax.lang.model.element.*
import javax.lang.model.type.DeclaredType
import javax.lang.model.type.TypeMirror
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic
/**
* The Realm Field Names Generator is a processor that looks at all available Realm model classes
* and create an companion class with easy, type-safe access to all field names.
*/
@SupportedAnnotationTypes("io.realm.annotations.RealmClass")
class RealmFieldNamesProcessor : AbstractProcessor() {
private val classes = HashSet<ClassData>()
lateinit private var typeUtils: Types
lateinit private var messager: Messager
lateinit private var elementUtils: Elements
private var ignoreAnnotation: TypeMirror? = null
private var realmClassAnnotation: TypeElement? = null
private var realmModelInterface: TypeMirror? = null
private var realmListClass: DeclaredType? = null
private var realmResultsClass: DeclaredType? = null
private var fileGenerator: FileGenerator? = null
private var done = false
@Synchronized override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
typeUtils = processingEnv.typeUtils!!
messager = processingEnv.messager!!
elementUtils = processingEnv.elementUtils!!
// If the Realm class isn't found something is wrong the project setup.
// Most likely Realm isn't on the class path, so just disable the
// annotation processor
val isRealmAvailable = elementUtils.getTypeElement("io.realm.Realm") != null
if (!isRealmAvailable) {
done = true
} else {
ignoreAnnotation = elementUtils.getTypeElement("io.realm.annotations.Ignore")?.asType()
realmClassAnnotation = elementUtils.getTypeElement("io.realm.annotations.RealmClass")
realmModelInterface = elementUtils.getTypeElement("io.realm.RealmModel")?.asType()
realmListClass = typeUtils.getDeclaredType(elementUtils.getTypeElement("io.realm.RealmList"),
typeUtils.getWildcardType(null, null))
realmResultsClass = typeUtils.getDeclaredType(elementUtils.getTypeElement("io.realm.RealmResults"),
typeUtils.getWildcardType(null, null))
fileGenerator = FileGenerator(processingEnv.filer)
}
}
override fun getSupportedSourceVersion(): SourceVersion {
return SourceVersion.latestSupported()
}
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
if (done) {
return CONSUME_ANNOTATIONS
}
// Create all proxy classes
roundEnv.getElementsAnnotatedWith(realmClassAnnotation).forEach { classElement ->
if (typeUtils.isAssignable(classElement.asType(), realmModelInterface)) {
val classData = processClass(classElement as TypeElement)
classes.add(classData)
}
}
// If a model class references a library class, the library class will not be part of this
// annotation processor round. For all those references we need to pull field information
// from the classpath instead.
val libraryClasses = HashMap<String, ClassData>()
classes.forEach {
it.fields.forEach { _, value ->
// Analyze the library class file the first time it is encountered.
if (value != null ) {
if (classes.all{ it.qualifiedClassName != value } && !libraryClasses.containsKey(value)) {
libraryClasses.put(value, processLibraryClass(value))
}
}
}
}
classes.addAll(libraryClasses.values)
done = fileGenerator!!.generate(classes)
return CONSUME_ANNOTATIONS
}
private fun processClass(classElement: TypeElement): ClassData {
val packageName = getPackageName(classElement)
val className = classElement.simpleName.toString()
val data = ClassData(packageName, className)
// Find all appropriate fields
classElement.enclosedElements.forEach {
val elementKind = it.kind
if (elementKind == ElementKind.FIELD) {
val variableElement = it as VariableElement
val modifiers = variableElement.modifiers
if (modifiers.contains(Modifier.STATIC)) {
return@forEach // completely ignore any static fields
}
// Don't add any fields marked with @Ignore
val ignoreField = variableElement.annotationMirrors
.map { it.annotationType.toString() }
.contains("io.realm.annotations.Ignore")
if (!ignoreField) {
data.addField(it.getSimpleName().toString(), getLinkedFieldType(it))
}
}
}
return data
}
private fun processLibraryClass(qualifiedClassName: String): ClassData {
val libraryClass = Class.forName(qualifiedClassName) // Library classes should be on the classpath
val packageName = libraryClass.`package`.name
val className = libraryClass.simpleName
val data = ClassData(packageName, className, libraryClass = true)
libraryClass.declaredFields.forEach { field ->
if (java.lang.reflect.Modifier.isStatic(field.modifiers)) {
return@forEach // completely ignore any static fields
}
// Add field if it is not being ignored.
if (field.annotations.all { it.toString() != "io.realm.annotations.Ignore" }) {
data.addField(field.name, field.type.name)
}
}
return data
}
/**
* Returns the qualified name of the linked Realm class field or `null` if it is not a linked
* class.
*/
private fun getLinkedFieldType(field: Element): String? {
if (typeUtils.isAssignable(field.asType(), realmModelInterface)) {
// Object link
val typeElement = elementUtils.getTypeElement(field.asType().toString())
return typeElement.qualifiedName.toString()
} else if (typeUtils.isAssignable(field.asType(), realmListClass) || typeUtils.isAssignable(field.asType(), realmResultsClass)) {
// List link or LinkingObjects
val fieldType = field.asType()
val typeArguments = (fieldType as DeclaredType).typeArguments
if (typeArguments.size == 0) {
return null
}
return typeArguments[0].toString()
} else {
return null
}
}
private fun getPackageName(classElement: TypeElement): String? {
val enclosingElement = classElement.enclosingElement
if (enclosingElement.kind != ElementKind.PACKAGE) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Could not determine the package name. Enclosing element was: " + enclosingElement.kind)
return null
}
val packageElement = enclosingElement as PackageElement
return packageElement.qualifiedName.toString()
}
companion object {
private const val CONSUME_ANNOTATIONS = false
}
}

View File

@ -0,0 +1 @@
dk.ilios.realmfieldnames.RealmFieldNamesProcessor,aggregating

View File

@ -0,0 +1 @@
dk.ilios.realmfieldnames.RealmFieldNamesProcessor

View File

@ -189,7 +189,7 @@ dependencies {
// Database
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
kapt project(":library:external:realmfieldnameshelper")
// Shared Preferences
implementation libs.androidx.preferenceKtx

View File

@ -13,6 +13,7 @@ include ':library:external:diff-match-patch'
include ':library:external:dialpad'
include ':library:external:textdrawable'
include ':library:external:autocomplete'
include ':library:external:realmfieldnameshelper'
include ':library:rustCrypto'
include ':matrix-sdk-android'