Skip to content

Commit 39bb67c

Browse files
authored
@NotNull as alias for @required (#5161)
This PR adds support for org.jetbrains.annotations.NotNull as an alias for @required. This means that Realm now understands the Kotlin type-system in the Realm schema. Previously we reported an error if you did @required RealmList<MyType> list = new RealmList<>();. It is a bit unclear why, since a RealmList is always non-null on managed objects. Since it made the interop with Kotlin kinda strange, I made a change so you can now do @required RealmList<MyType> list = new RealmLis<>() as well as val list : RealmList<MyType> = RealmList(); After this change it is no longer possible to do val person : Person either, you have to do val person : Person?. This reflects the constraints of Realm, but if people are able to maintain the variant themselves (hint: not possible when using sync), then they can use a custom non-null getter.
1 parent cea0474 commit 39bb67c

4 files changed

Lines changed: 60 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and `SyncUser#retrieveInfoForUserAsync` which returns a `SyncUserInfo` with mode
1616
* [ObjectServer] APIs of `UserStore` have been changed to support same user identity but different authentication server scenario.
1717
* [ObjectServer] Added `SyncUser.allSessions` to retrieve the all valid sessions belonging to the user (#4783).
1818
* Added `Nullable` annotation to methods that may return `null` in order to improve Kotlin usability. This also introduced a dependency to `com.google.code.findbugs:jsr305`.
19+
* `org.jetbrains.annotations.NotNull` is now an alias for `@Required`. This means that the Realm Schema now fully understand Kotlin non-null types.
1920
* Added support for new data type `MutableRealmIntegers`. The new type behaves almost exactly as a reference to a Long (mutable nullable, etc) but supports `increment` and `decrement` methods, which implement a Conflict Free Replicated Data Type, whose value will converge even when changed across distributed devices with poor connections (#4266).
2021
* Added more detailed exception message for `RealmMigrationNeeded`.
2122
* Bumping schema version only without any actual schema changes will just succeed even when the migration block is not supplied. It threw an `RealmMigrationNeededException` before in the same case.

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Set;
2626

2727
import javax.annotation.processing.ProcessingEnvironment;
28+
import javax.lang.model.element.AnnotationMirror;
2829
import javax.lang.model.element.Element;
2930
import javax.lang.model.element.ElementKind;
3031
import javax.lang.model.element.ExecutableElement;
@@ -375,7 +376,7 @@ private boolean categorizeField(Element element) {
375376
if (!categorizeIndexField(element, field)) { return false; }
376377
}
377378

378-
if (field.getAnnotation(Required.class) != null) {
379+
if (isRequiredField(field)) {
379380
categorizeRequiredField(element, field);
380381
} else {
381382
// The field doesn't have the @Required annotation.
@@ -408,6 +409,23 @@ private boolean categorizeField(Element element) {
408409
return true;
409410
}
410411

412+
private boolean isRequiredField(VariableElement field) {
413+
if (field.getAnnotation(Required.class) != null) {
414+
return true;
415+
}
416+
417+
// Kotlin uses the `org.jetbrains.annotations.NotNull` annotation to mark non-null fields.
418+
// In order to fully support the Kotlin type system we interpret `@NotNull` as an alias
419+
// for `@Required`
420+
for (AnnotationMirror annotation : field.getAnnotationMirrors()) {
421+
if (annotation.getAnnotationType().toString().equals("org.jetbrains.annotations.NotNull")) {
422+
return true;
423+
}
424+
}
425+
426+
return false;
427+
}
428+
411429
// The field has the @Index annotation. It's only valid for column types:
412430
// STRING, DATE, INTEGER, BOOLEAN, and RealmMutableInteger
413431
private boolean categorizeIndexField(Element element, VariableElement variableElement) {
@@ -441,13 +459,13 @@ private boolean categorizeIndexField(Element element, VariableElement variableEl
441459
private void categorizeRequiredField(Element element, VariableElement variableElement) {
442460
if (Utils.isPrimitiveType(variableElement)) {
443461
Utils.error(String.format(Locale.US,
444-
"@Required annotation is unnecessary for primitive field \"%s\".", element));
462+
"@Required or @NotNull annotation is unnecessary for primitive field \"%s\".", element));
445463
return;
446464
}
447465

448-
if (Utils.isRealmList(variableElement) || Utils.isRealmModel(variableElement)) {
466+
if (Utils.isRealmModel(variableElement)) {
449467
Utils.error(String.format(Locale.US,
450-
"Field \"%s\" with type \"%s\" cannot be @Required.", element, element.asType()));
468+
"Field \"%s\" with type \"%s\" cannot be @Required or @NotNull.", element, element.asType()));
451469
return;
452470
}
453471

realm/realm-library/src/androidTest/kotlin/io/realm/KotlinSchemaTests.kt

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,37 @@ class KotlinSchemaTests {
5151
fun kotlinTypeNonNull() {
5252
val objSchema = realm.schema.get(AllKotlinTypes::class.simpleName)!!
5353

54-
// Document current nullability. Ideally all should be non-nullable. This is currently
55-
// not the case.
56-
// TODO We should fix this. Tracked by https://github.com/realm/realm-java/issues/4701
57-
assertTrue(objSchema.isNullable(AllKotlinTypes::nonNullBinary.name));
54+
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullBinary.name));
5855
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullBoolean.name));
59-
assertTrue(objSchema.isNullable(AllKotlinTypes::nonNullString.name));
56+
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullString.name));
6057
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullLong.name));
6158
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullInt.name));
6259
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullShort.name));
6360
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullByte.name));
64-
assertTrue(objSchema.isNullable(AllKotlinTypes::nonNullDate.name));
61+
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullDate.name));
6562
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullDouble.name));
6663
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullFloat.name));
6764
assertFalse(objSchema.isNullable(AllKotlinTypes::nonNullList.name));
68-
assertTrue(objSchema.isNullable(AllKotlinTypes::nonNullObject.name));
65+
// We cannot enforce this constraint inside the schema right now.
66+
// If people maintain the variant themselves they need a custom getter
67+
// assertTrue(objSchema.isNullable(AllKotlinTypes::nonNullObject.name));
6968
}
70-
}
69+
70+
@Test
71+
fun kotlinTypeNull() {
72+
val objSchema = realm.schema.get(AllKotlinTypes::class.simpleName)!!
73+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullBinary.name));
74+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullBoolean.name));
75+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullString.name));
76+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullLong.name));
77+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullInt.name));
78+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullShort.name));
79+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullByte.name));
80+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullDate.name));
81+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullDouble.name));
82+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullFloat.name));
83+
assertFalse(objSchema.isNullable(AllKotlinTypes::nullList.name)); // Managed realm objects do not allow null lists
84+
assertTrue(objSchema.isNullable(AllKotlinTypes::nullObject.name));
85+
}
86+
87+
}

realm/realm-library/src/androidTest/kotlin/io/realm/entities/AllKotlinTypes.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,23 @@ open class AllKotlinTypes : RealmObject() {
6262
var nullBinary: ByteArray? = null
6363
var nonNullBinary: ByteArray = ByteArray(0)
6464

65-
// This turns into Byte[] which we dont support for some reason?
66-
// var nullBoxedBinary: Array<Byte>? = null
67-
// var nonNullBoxedBinary: Array<Byte> = emptyArray()
65+
// This turns into Byte[] which we dont support for some reason?
66+
// var nullBoxedBinary: Array<Byte>? = null
67+
// var nonNullBoxedBinary: Array<Byte> = emptyArray()
6868

6969
var nullObject: AllKotlinTypes? = null
70-
var nonNullObject: AllKotlinTypes = AllKotlinTypes()
7170

72-
var nullList: RealmList<AllKotlinTypes>? = null // This should not be allowed
71+
// Not-null object references cannot be enforced generically at the schema level, e.g. sync that
72+
// removes an object reference.
73+
// If people can maintain the variant themselves they can just expose a custom non-null getter.
74+
// var nonNullObject: AllKotlinTypes = AllKotlinTypes()
75+
76+
// This is only possible in unmanaged objects, managed objects are never null
77+
// For now we allow this anyway.
78+
var nullList: RealmList<AllKotlinTypes>? = null
7379
var nonNullList: RealmList<AllKotlinTypes> = RealmList()
7480

75-
@LinkingObjects("nonNullObject")
81+
@LinkingObjects("nullObject")
7682
val objectParents: RealmResults<AllKotlinTypes>? = null;
7783

7884
@LinkingObjects("nonNullList")

0 commit comments

Comments
 (0)