Search code examples
kotlinektorp

Kotlin mutators


I'm using Ektorp as CouchDB "ORM" but this question seems to be more general than that. Could someone explain what is the difference between

@JsonInclude(NON_NULL)
abstract class AbstractEntity(
        id: String?,
        revision: String?
) {

    @JsonProperty("_id")
    val id: String? = id

    @JsonProperty("_rev")
    val revision: String? = revision

}

and

@JsonInclude(NON_NULL)
abstract class AbstractEntity(
        @JsonProperty("_id")
        val id: String? = null,
        @JsonProperty("_rev")
        val revision: String? = null
)

?

For the first case Ektorp does not complain but for the second it says:

org.ektorp.InvalidDocumentException: Cannot resolve revision mutator in class com.scherule.calendaring.domain.Meeting

    at org.ektorp.util.Documents$MethodAccessor.assertMethodFound(Documents.java:165)
    at org.ektorp.util.Documents$MethodAccessor.<init>(Documents.java:144)
    at org.ektorp.util.Documents.getAccessor(Documents.java:113)
    at org.ektorp.util.Documents.getRevision(Documents.java:77)
    at org.ektorp.util.Documents.isNew(Documents.java:85)

In addition to that if I do

@JsonInclude(NON_NULL)
abstract class AbstractEntity(
        @JsonProperty("_id")
        var id: String? = null,
        @JsonProperty("_rev")
        var revision: String? = null
)

I get:

org.ektorp.DbAccessException: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "_id" (class com.scherule.calendaring.domain.Meeting), not marked as ignorable

The question is in what way the first code snippet is different from the second? In the first case Ektorp "thinks" there is a mutator but not in the second...

Here's the code snippet which Ektorp seems to use when locating a method (setId, setRevision).

private Method findMethod(Class<?> clazz, String name,
                Class<?>... parameters) throws Exception {
            for (Method me : clazz.getDeclaredMethods()) {
                if (me.getName().equals(name)
                        && me.getParameterTypes().length == parameters.length) {
                    me.setAccessible(true);
                    return me;
                }
            }
            return clazz.getSuperclass() != null ? findMethod(
                    clazz.getSuperclass(), name, parameters) : null;
        }

Solution

  • The difference is how application of the annotations to properties defined in the class body differs from annotating properties declared in a primary constructor.

    See: Annotation Use-site Targets in the language reference:

    When you're annotating a property or a primary constructor parameter, there are multiple Java elements which are generated from the corresponding Kotlin element, and therefore multiple possible locations for the annotation in the generated Java bytecode.

    If you don't specify a use-site target, the target is chosen according to the @Target annotation of the annotation being used. If there are multiple applicable targets, the first applicable target from the following list is used:

    • param
    • property
    • field

    So, when you annotate a property declared in a primary constructor, it's the constructor parameter that by default gets the annotation in the Java bytecode. To alter that, specify the annotation target explicitly, for example:

    abstract class AbstractEntity(
        @get:JsonProperty("_id")
        val id: String? = null,
        @get:JsonProperty("_rev")
        val revision: String? = null
    )