Import source from https://github.com/cmelchior/realmfieldnameshelper
This commit is contained in:
parent
cd292488b6
commit
ff09ba1208
|
@ -239,8 +239,6 @@ ext.groups = [
|
||||||
regex: [
|
regex: [
|
||||||
],
|
],
|
||||||
group: [
|
group: [
|
||||||
// https://github.com/cmelchior/realmfieldnameshelper/issues/42
|
|
||||||
'dk.ilios',
|
|
||||||
'im.dlg',
|
'im.dlg',
|
||||||
'me.dm7.barcodescanner',
|
'me.dm7.barcodescanner',
|
||||||
'me.gujun.android',
|
'me.gujun.android',
|
||||||
|
|
|
@ -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
|
// Database
|
||||||
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
|
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
|
||||||
|
|
||||||
kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
|
kapt project(":library:external:realmfieldnameshelper")
|
||||||
|
|
||||||
// Shared Preferences
|
// Shared Preferences
|
||||||
implementation libs.androidx.preferenceKtx
|
implementation libs.androidx.preferenceKtx
|
||||||
|
|
|
@ -13,6 +13,7 @@ include ':library:external:diff-match-patch'
|
||||||
include ':library:external:dialpad'
|
include ':library:external:dialpad'
|
||||||
include ':library:external:textdrawable'
|
include ':library:external:textdrawable'
|
||||||
include ':library:external:autocomplete'
|
include ':library:external:autocomplete'
|
||||||
|
include ':library:external:realmfieldnameshelper'
|
||||||
|
|
||||||
include ':library:rustCrypto'
|
include ':library:rustCrypto'
|
||||||
include ':matrix-sdk-android'
|
include ':matrix-sdk-android'
|
||||||
|
|
Loading…
Reference in New Issue