Skip to content

Commit 57d24b4

Browse files
authored
Dictionary data type (#7397)
1 parent a1b984d commit 57d24b4

File tree

109 files changed

+17928
-2877
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+17928
-2877
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* Added support for `java.util.UUID` as supported field in model classes.
1212
* Added support for `java.util.UUID` as a primary key.
1313
* Added support for `RealmAny` as supported field in model classes. A `RealmAny` is used to represent a polymorphic Realm value or Realm Object, is indexable but cannot be used as a primary key. See [Javadoc for RealmAny](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmAny.html).
14+
* Added support for `RealmDictionary` as supported field in model classes. A `RealmDictionary` is a `Map` of strings to values - all types under the `RealmAny` umbrella can be used as values. See [Javadoc for RealmDictionary](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmDictionary.html) and [Javadoc for RealmMap](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmMap.html). `RealmDictionary` is not yet supported by any of the `Realm.insert` and `Realm.createFromJson` methods - This support will be added in a future release.
1415
* Allow UTF8 encoded characters in property names in string-based queries ([#4467](https://github.com/realm/realm-core/issues/4467))
1516
* The error message when the initial steps of opening a Realm file fails is now more descriptive.
1617
* Make conversion of Decimal128 to/from string work for numbers with more than 19 significant digits. ([#4548](https://github.com/realm/realm-core/issues/4548))

dependencies.list

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ REALM_CORE=10.3.3
44

55
# Version of MongoDB Realm used by integration tests
66
# See https://github.com/realm/ci/packages/147854 for available versions
7-
MONGODB_REALM_SERVER=2021-03-29
7+
MONGODB_REALM_SERVER=2021-04-19
88

99
# Common Android settings across projects
1010
GRADLE_BUILD_TOOLS=4.0.0

realm/realm-annotations-processor/src/main/java/io/realm/processor/ClassMetaData.kt

Lines changed: 124 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
4040
private val backlinks = LinkedHashSet<Backlink>()
4141
private val nullableFields = LinkedHashSet<RealmFieldElement>() // Set of fields which can be nullable
4242
private val nullableValueListFields = LinkedHashSet<RealmFieldElement>() // Set of fields whose elements can be nullable
43+
private val nullableValueMapFields = LinkedHashSet<RealmFieldElement>() // Set of fields whose elements can be nullable
4344

4445
// package name for model class.
4546
private lateinit var packageName: String
@@ -59,7 +60,7 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
5960
lateinit var internalClassName: String
6061
private set
6162

62-
private val validPrimaryKeyTypes: List<TypeMirror> = Arrays.asList(
63+
private val validPrimaryKeyTypes: List<TypeMirror> = listOf(
6364
typeMirrors.STRING_MIRROR,
6465
typeMirrors.PRIMITIVE_LONG_MIRROR,
6566
typeMirrors.PRIMITIVE_INT_MIRROR,
@@ -68,7 +69,7 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
6869
typeMirrors.OBJECT_ID_MIRROR,
6970
typeMirrors.UUID_MIRROR
7071
)
71-
private val validListValueTypes: List<TypeMirror> = Arrays.asList(
72+
private val validListValueTypes: List<TypeMirror> = listOf(
7273
typeMirrors.STRING_MIRROR,
7374
typeMirrors.BINARY_MIRROR,
7475
typeMirrors.BOOLEAN_MIRROR,
@@ -84,6 +85,23 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
8485
typeMirrors.UUID_MIRROR,
8586
typeMirrors.MIXED_MIRROR
8687
)
88+
private val validDictionaryTypes: List<TypeMirror> = listOf(
89+
typeMirrors.STRING_MIRROR,
90+
typeMirrors.BINARY_MIRROR,
91+
typeMirrors.BINARY_NON_PRIMITIVE_MIRROR,
92+
typeMirrors.BOOLEAN_MIRROR,
93+
typeMirrors.LONG_MIRROR,
94+
typeMirrors.INTEGER_MIRROR,
95+
typeMirrors.SHORT_MIRROR,
96+
typeMirrors.BYTE_MIRROR,
97+
typeMirrors.DOUBLE_MIRROR,
98+
typeMirrors.FLOAT_MIRROR,
99+
typeMirrors.DATE_MIRROR,
100+
typeMirrors.DECIMAL128_MIRROR,
101+
typeMirrors.OBJECT_ID_MIRROR,
102+
typeMirrors.UUID_MIRROR,
103+
typeMirrors.MIXED_MIRROR
104+
)
87105
private val stringType = typeMirrors.STRING_MIRROR
88106

89107
private val typeUtils: Types = env.typeUtils
@@ -205,6 +223,15 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
205223
return nullableValueListFields.contains(realmListVariableElement)
206224
}
207225

226+
/**
227+
* Checks if the value of a `RealmDictionary` entry designated by `realmDictionaryVariableElement` is nullable.
228+
*
229+
* @return `true` if the element is nullable type, `false` otherwise.
230+
*/
231+
fun isDictionaryValueNullable(realmDictionaryVariableElement: VariableElement): Boolean {
232+
return nullableValueMapFields.contains(realmDictionaryVariableElement)
233+
}
234+
208235
/**
209236
* Checks if a VariableElement is indexed.
210237
*
@@ -303,6 +330,9 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
303330
if (!checkCollectionTypes()) {
304331
return false
305332
}
333+
if (!checkDictionaryTypes()) {
334+
return false
335+
}
306336
if (!checkReferenceTypes()) {
307337
return false
308338
}
@@ -361,44 +391,88 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
361391
return true
362392
}
363393

364-
private fun checkRealmListType(field: VariableElement): Boolean {
394+
private fun checkDictionaryTypes(): Boolean {
395+
for (field in fields) {
396+
if (Utils.isRealmDictionary(field)) {
397+
if (!checkDictionaryValuesType(field)) {
398+
return false
399+
}
400+
}
401+
}
402+
403+
return true
404+
}
405+
406+
private fun checkDictionaryValuesType(field: VariableElement): Boolean {
365407
// Check for missing generic (default back to Object)
366408
if (Utils.getGenericTypeQualifiedName(field) == null) {
367409
Utils.error(getFieldErrorSuffix(field) + "No generic type supplied for field", field)
368410
return false
369411
}
370412

371-
// Check that the referenced type is a concrete class and not an interface
372-
val fieldType = field.asType()
373-
val elementTypeMirror = (fieldType as DeclaredType).typeArguments[0]
374-
if (elementTypeMirror.kind == TypeKind.DECLARED /* class of interface*/) {
375-
val elementTypeElement = (elementTypeMirror as DeclaredType).asElement() as TypeElement
376-
if (elementTypeElement.superclass.kind == TypeKind.NONE) {
377-
Utils.error(
378-
getFieldErrorSuffix(field) + "Only concrete Realm classes are allowed in RealmLists. "
379-
+ "Neither interfaces nor abstract classes are allowed.",
380-
field)
381-
return false
382-
}
413+
val elementTypeMirror = checkReferenceIsNotInterface(field) ?: return false
414+
return checkAcceptableClass(
415+
field,
416+
elementTypeMirror,
417+
validDictionaryTypes,
418+
"Element type of RealmDictionary must be of type 'RealmAny' or any type that can be boxed inside 'RealmAny': "
419+
)
420+
}
421+
422+
private fun checkRealmListType(field: VariableElement): Boolean {
423+
// Check for missing generic (default back to Object)
424+
if (Utils.getGenericTypeQualifiedName(field) == null) {
425+
Utils.error(getFieldErrorSuffix(field) + "No generic type supplied for field", field)
426+
return false
383427
}
384428

429+
val elementTypeMirror = checkReferenceIsNotInterface(field) ?: return false
430+
return checkAcceptableClass(
431+
field,
432+
elementTypeMirror,
433+
validListValueTypes,
434+
"Element type of RealmList must be a class implementing 'RealmModel' or one of "
435+
)
436+
}
437+
438+
private fun checkAcceptableClass(
439+
field: VariableElement,
440+
elementTypeMirror: TypeMirror,
441+
validTypes: List<TypeMirror>,
442+
specificFieldTypeMessage: String
443+
): Boolean {
385444
// Check if the actual value class is acceptable
386-
if (!containsType(validListValueTypes, elementTypeMirror) && !Utils.isRealmModel(elementTypeMirror)) {
387-
val messageBuilder = StringBuilder(
388-
getFieldErrorSuffix(field) + "Element type of RealmList must be a class implementing 'RealmModel' or one of ")
445+
if (!containsType(validTypes, elementTypeMirror) && !Utils.isRealmModel(elementTypeMirror)) {
446+
val messageBuilder = StringBuilder(getFieldErrorSuffix(field) + "Type was '$elementTypeMirror'. $specificFieldTypeMessage")
389447
val separator = ", "
390-
for (type in validListValueTypes) {
448+
for (type in validTypes) {
391449
messageBuilder.append('\'').append(type.toString()).append('\'').append(separator)
392450
}
393451
messageBuilder.setLength(messageBuilder.length - separator.length)
394452
messageBuilder.append('.')
395453
Utils.error(messageBuilder.toString(), field)
396454
return false
397455
}
398-
399456
return true
400457
}
401458

459+
private fun checkReferenceIsNotInterface(field: VariableElement): TypeMirror? {
460+
// Check that the referenced type is a concrete class and not an interface
461+
val fieldType = field.asType()
462+
val elementTypeMirror: TypeMirror = (fieldType as DeclaredType).typeArguments[0]
463+
if (elementTypeMirror.kind == TypeKind.DECLARED /* class of interface*/) {
464+
val elementTypeElement = (elementTypeMirror as DeclaredType).asElement() as TypeElement
465+
if (elementTypeElement.superclass.kind == TypeKind.NONE) {
466+
Utils.error(
467+
getFieldErrorSuffix(field) + "Only concrete Realm classes are allowed in field '$field'. "
468+
+ "Neither interfaces nor abstract classes are allowed.",
469+
field)
470+
return null
471+
}
472+
}
473+
return elementTypeMirror
474+
}
475+
402476
private fun checkRealmResultsType(field: VariableElement): Boolean {
403477
// Only classes implementing RealmModel are allowed since RealmResults field is used only for backlinks.
404478

@@ -467,11 +541,12 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
467541
if (!field.modifiers.contains(Modifier.FINAL)) {
468542
continue
469543
}
470-
if (Utils.isRealmList(field) || Utils.isMutableRealmInteger(field)) {
544+
if (Utils.isRealmList(field) || Utils.isMutableRealmInteger(field) ||
545+
Utils.isRealmDictionary(field)) {
471546
continue
472547
}
473548

474-
Utils.error(String.format(Locale.US, "Class \"%s\" contains illegal final field \"%s\".", simpleJavaClassName,
549+
Utils.error(String.format(Locale.US, "Class \"%s\" contains illegal final/immutable field \"%s\".", simpleJavaClassName,
475550
field.simpleName.toString()))
476551

477552
return false
@@ -515,7 +590,7 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
515590
}
516591
}
517592

518-
// @Required annotation of RealmList field only affects its value type, not field itself.
593+
// @Required annotation of RealmList and RealmDictionary field only affects its value type, not field itself.
519594
if (Utils.isRealmList(field)) {
520595
val hasRequiredAnnotation = hasRequiredAnnotation(field)
521596
val listGenericType = (field.asType() as DeclaredType).typeArguments
@@ -535,6 +610,26 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
535610
nullableValueListFields.add(field)
536611
}
537612
}
613+
} else if (Utils.isRealmDictionary(field)) {
614+
// Same as RealmList
615+
val hasRequiredAnnotation = hasRequiredAnnotation(field)
616+
val listGenericType = (field.asType() as DeclaredType).typeArguments
617+
val containsRealmModelClasses = (listGenericType.isNotEmpty() && Utils.isRealmModel(listGenericType[0]))
618+
val containsRealmAny = (listGenericType.isNotEmpty() && Utils.isRealmAny(listGenericType[0]))
619+
620+
// @Required not allowed if the dictionary contains Realm model classes
621+
if (hasRequiredAnnotation && (containsRealmModelClasses || containsRealmAny)) {
622+
Utils.error("@Required not allowed on RealmDictionaries that contain other Realm model classes and RealmAny.")
623+
return false
624+
}
625+
626+
// @Required thus only makes sense for RealmDictionaries with primitive types
627+
// We only check @Required annotation. @org.jetbrains.annotations.NotNull annotation should not affect nullability of the list values.
628+
if (!hasRequiredAnnotation) {
629+
if (!containsRealmModelClasses) {
630+
nullableValueMapFields.add(field)
631+
}
632+
}
538633
} else if (isRequiredField(field)) {
539634
if (!checkBasicRequiredAnnotationUsage(field)) {
540635
return false
@@ -570,7 +665,12 @@ class ClassMetaData(env: ProcessingEnvironment, typeMirrors: TypeMirrors, privat
570665

571666
// Standard field that appears to be valid (more fine grained checks might fail later).
572667
fields.add(field)
573-
if (Utils.isRealmModel(field) || Utils.isRealmModelList(field) || Utils.isRealmAnyList(field) || Utils.isRealmAny(field)) {
668+
if (Utils.isRealmModel(field) ||
669+
Utils.isRealmModelList(field) ||
670+
Utils.isRealmAnyList(field) ||
671+
Utils.isRealmAny(field) ||
672+
Utils.isRealmModelDictionary(field) ||
673+
Utils.isRealmAnyDictionary(field)) {
574674
_objectReferenceFields.add(field)
575675
} else {
576676
basicTypeFields.add(field)

realm/realm-annotations-processor/src/main/java/io/realm/processor/Constants.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,23 @@ object Constants {
7070
"io.realm.RealmAny" to RealmFieldType.MIXED_LIST
7171
)
7272

73+
val DICTIONARY_ELEMENT_TYPE_TO_REALM_TYPES = mapOf(
74+
"java.lang.Byte" to RealmFieldType.STRING_TO_INTEGER_MAP,
75+
"java.lang.Short" to RealmFieldType.STRING_TO_INTEGER_MAP,
76+
"java.lang.Integer" to RealmFieldType.STRING_TO_INTEGER_MAP,
77+
"java.lang.Long" to RealmFieldType.STRING_TO_INTEGER_MAP,
78+
"java.lang.Float" to RealmFieldType.STRING_TO_FLOAT_MAP,
79+
"java.lang.Double" to RealmFieldType.STRING_TO_DOUBLE_MAP,
80+
"java.lang.Boolean" to RealmFieldType.STRING_TO_BOOLEAN_MAP,
81+
"java.lang.String" to RealmFieldType.STRING_TO_STRING_MAP,
82+
"java.util.Date" to RealmFieldType.STRING_TO_DATE_MAP,
83+
"byte[]" to RealmFieldType.STRING_TO_BINARY_MAP,
84+
"org.bson.types.Decimal128" to RealmFieldType.STRING_TO_DECIMAL128_MAP,
85+
"org.bson.types.ObjectId" to RealmFieldType.STRING_TO_OBJECT_ID_MAP,
86+
"java.util.UUID" to RealmFieldType.STRING_TO_UUID_MAP,
87+
"io.realm.RealmAny" to RealmFieldType.STRING_TO_MIXED_MAP
88+
)
89+
7390
/**
7491
* Realm types and their corresponding Java types.
7592
*
@@ -105,7 +122,21 @@ object Constants {
105122
DECIMAL128_LIST("DECIMAL128_LIST", "List"),
106123
OBJECT_ID_LIST("OBJECT_ID_LIST", "List"),
107124
UUID_LIST("UUID_LIST", "List"),
108-
MIXED_LIST("MIXED_LIST", "List");
125+
MIXED_LIST("MIXED_LIST", "List"),
126+
127+
STRING_TO_LINK_MAP("STRING_TO_LINK_MAP", "Map"),
128+
STRING_TO_INTEGER_MAP("STRING_TO_INTEGER_MAP", "Map"),
129+
STRING_TO_BOOLEAN_MAP("STRING_TO_BOOLEAN_MAP", "Map"),
130+
STRING_TO_STRING_MAP("STRING_TO_STRING_MAP", "Map"),
131+
STRING_TO_BINARY_MAP("STRING_TO_BINARY_MAP", "Map"),
132+
STRING_TO_DATE_MAP("STRING_TO_DATE_MAP", "Map"),
133+
STRING_TO_FLOAT_MAP("STRING_TO_FLOAT_MAP", "Map"),
134+
STRING_TO_DOUBLE_MAP("STRING_TO_DOUBLE_MAP", "Map"),
135+
STRING_TO_DECIMAL128_MAP("STRING_TO_DECIMAL128_MAP", "Map"),
136+
STRING_TO_OBJECT_ID_MAP("STRING_TO_OBJECT_ID_MAP", "Map"),
137+
STRING_TO_UUID_MAP("STRING_TO_UUID_MAP", "Map"),
138+
STRING_TO_MIXED_MAP("STRING_TO_MIXED_MAP", "Map");
139+
109140

110141
/**
111142
* The name of the enum, used in the Java bindings, used to represent the corresponding type.

realm/realm-annotations-processor/src/main/java/io/realm/processor/OsObjectBuilderTypeHelper.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,27 @@ object OsObjectBuilderTypeHelper {
2525

2626
private val QUALIFIED_TYPE_TO_BUILDER: Map<QualifiedClassName, String>
2727
private val QUALIFIED_LIST_TYPE_TO_BUILDER: Map<QualifiedClassName, String>
28+
private val QUALIFIED_MAP_VALUES: Map<QualifiedClassName, String> = mapOf(
29+
QualifiedClassName("io.realm.RealmAny") to "RealmAnyValueDictionary",
30+
QualifiedClassName("java.lang.Boolean") to "BooleanValueDictionary",
31+
QualifiedClassName("java.lang.String") to "StringValueDictionary",
32+
QualifiedClassName("java.lang.Integer") to "IntegerValueDictionary",
33+
QualifiedClassName("java.lang.Float") to "FloatValueDictionary",
34+
QualifiedClassName("java.lang.Long") to "LongValueDictionary",
35+
QualifiedClassName("java.lang.Short") to "ShortValueDictionary",
36+
QualifiedClassName("java.lang.Byte") to "ByteValueDictionary",
37+
QualifiedClassName("java.lang.Double") to "DoubleValueDictionary",
38+
QualifiedClassName("java.util.Date") to "DateValueDictionary",
39+
QualifiedClassName("byte[]") to "BinaryValueDictionary",
40+
QualifiedClassName("org.bson.types.ObjectId") to "ObjectIdValueDictionary",
41+
QualifiedClassName("org.bson.types.Decimal128") to "Decimal128ValueDictionary",
42+
QualifiedClassName("java.util.UUID") to "UUIDValueDictionary"
43+
)
2844

2945
init {
3046
// Map of qualified types to their OsObjectBuilder Type
3147
val fieldTypes = HashMap<QualifiedClassName, String>()
32-
fieldTypes.apply {
48+
fieldTypes.apply {
3349
this[QualifiedClassName("byte")] = "Integer"
3450
this[QualifiedClassName("byte")] = "Integer"
3551
this[QualifiedClassName("short")] = "Integer"
@@ -89,6 +105,8 @@ object OsObjectBuilderTypeHelper {
89105
"addObjectList"
90106
} else if (Utils.isRealmValueList(field)) {
91107
"add" + getListTypeName(Utils.getRealmListType(field))
108+
} else if (Utils.isRealmDictionary(field)) {
109+
"add" + getDictionaryValueTypeName(Utils.getDictionaryType(field))
92110
} else if (Utils.isRealmResults(field)) {
93111
throw IllegalStateException("RealmResults are not supported by OsObjectBuilder: $field")
94112
} else {
@@ -112,4 +130,9 @@ object OsObjectBuilderTypeHelper {
112130
throw IllegalArgumentException("Unsupported list type: $type")
113131
}
114132

133+
private fun getDictionaryValueTypeName(typeName: QualifiedClassName?): String {
134+
return requireNotNull(QUALIFIED_MAP_VALUES[typeName]) {
135+
"Unsupported dictionary value type: '$typeName'"
136+
}
137+
}
115138
}

0 commit comments

Comments
 (0)