From ff09ba1208fb50b29dba464e693a65a1367063a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 23 Jun 2023 17:58:51 +0200 Subject: [PATCH] Import source from https://github.com/cmelchior/realmfieldnameshelper --- dependencies_groups.gradle | 2 - .../realmfieldnameshelper/build.gradle | 23 +++ .../dk/ilios/realmfieldnames/ClassData.kt | 24 +++ .../realmfieldnames/FieldNameFormatter.kt | 75 +++++++ .../dk/ilios/realmfieldnames/FileGenerator.kt | 82 ++++++++ .../RealmFieldNamesProcessor.kt | 187 ++++++++++++++++++ .../gradle/incremental.annotation.processors | 1 + .../javax.annotation.processing.Processor | 1 + matrix-sdk-android/build.gradle | 2 +- settings.gradle | 1 + 10 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 library/external/realmfieldnameshelper/build.gradle create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt create mode 100644 library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt create mode 100644 library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors create mode 100644 library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 6ca6c00dfa..35ba4b41a9 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -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', diff --git a/library/external/realmfieldnameshelper/build.gradle b/library/external/realmfieldnameshelper/build.gradle new file mode 100644 index 0000000000..13d8de7ca3 --- /dev/null +++ b/library/external/realmfieldnameshelper/build.gradle @@ -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' +} + diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt new file mode 100644 index 0000000000..d683a2adef --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt @@ -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() // + + 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 + } + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt new file mode 100644 index 0000000000..fbb44d333b --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FieldNameFormatter.kt @@ -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 "<class>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) + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt new file mode 100644 index 0000000000..02bdc70f44 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt @@ -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 "<class>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): Boolean { + return fileData + .filter { !it.libraryClass } + .all { generateFile(it, fileData) } + } + + private fun generateFile(classData: ClassData, classPool: Set): 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) + } +} diff --git a/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt new file mode 100644 index 0000000000..6f82882333 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/RealmFieldNamesProcessor.kt @@ -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() + 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, 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() + 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 + } +} diff --git a/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors b/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 0000000000..57897c8297 --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor,aggregating \ No newline at end of file diff --git a/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..58fadd699c --- /dev/null +++ b/library/external/realmfieldnameshelper/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +dk.ilios.realmfieldnames.RealmFieldNamesProcessor \ No newline at end of file diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 7420fba45e..82bb0b31d5 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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 diff --git a/settings.gradle b/settings.gradle index b3ecd410d6..7b57b530f7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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'