mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 03:48:12 +03:00
Import source from https://github.com/cmelchior/realmfieldnameshelper
This commit is contained in:
parent
cd292488b6
commit
ff09ba1208
10 changed files with 395 additions and 3 deletions
|
@ -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',
|
||||
|
|
23
library/external/realmfieldnameshelper/build.gradle
vendored
Normal file
23
library/external/realmfieldnameshelper/build.gradle
vendored
Normal 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'
|
||||
}
|
||||
|
24
library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt
vendored
Normal file
24
library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/ClassData.kt
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
82
library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt
vendored
Normal file
82
library/external/realmfieldnameshelper/src/main/kotlin/dk/ilios/realmfieldnames/FileGenerator.kt
vendored
Normal 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 "<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<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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
dk.ilios.realmfieldnames.RealmFieldNamesProcessor,aggregating
|
|
@ -0,0 +1 @@
|
|||
dk.ilios.realmfieldnames.RealmFieldNamesProcessor
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue