2
\$\begingroup\$

I am mapping a complex JSON response to two JPA Entity model classes using Jackson. The classes are CxExport and Mention, Mention has a Many to one relationship with CxExport I.e. Many mentions belong to one CxExport. The HTTP Response returns a List of mentions 2. The JPA model is a flattened instance of one of the list of mention objects returned in the HTTP response. the Mention JPA Entitys model's field names are different from the ones returned in the JSON response,

my code:

enum class ExportType(var type: Int){
        mention(0),
        dashboard(1)
}


@SequenceGenerator(name = "seq_cx_export", sequenceName = "SEQ_CX_EXPORT", allocationSize = 1, initialValue = 1)
@Access(AccessType.FIELD)
@Entity
class CxExport(
        @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_cx_export")
        override var pk_id: Long? = null,


        var accountId: Long? = null,

        @Enumerated
        @Column(columnDefinition = "smallint")
        var exportType: ExportType? = null,
        var exportCount: Int? = null,
        var exportStart: Timestamp? = null,
        var exportEnd: Timestamp? = null

) : AbstractJpaPersistable<Long>() {}

@SequenceGenerator(name = "seq_ment", sequenceName = "SEQ_MENTION", allocationSize = 1, initialValue = 1)
@Access(AccessType.FIELD)
@Entity
class Mention(
        @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_ment")
        override var pk_id: Long? = null,


        @ManyToOne(cascade = [(CascadeType.MERGE)])
        @JoinColumn(name = "export_id")
        var cxExport: CxExport,


    var authorId: String? = null,
    var authorUrl: String? = null,
    var authorImg: String? = null,
    var authorTags: String?,


    var origAuthorName: String? = null,

    var messageTitle: String? = null,

        @Lob
    var messageContent: String? = null,

    var messageLanguage: String? = null,
    var messageSentiment: String? = null,
    var messageType: String? = null,

    var sourceId: String? = null,
    var sourceCategory: String? = null,
    var sourceType: String? = null,
    var sourceDomain: String? = null,
    var sourceUrl: String? = null,
    var sourceProfile: String? = null,
    var sourceProfileName: String? = null,

    var locContinent: String? = null,
    var locCountry: String? = null,
    var locCity: String? = null,
    var locRegion: String? = null,
    var locLongitude: String? = null,
    var locLatitude: String? = null,




    var permalink: String? = null,


        var priorityId: Int? = null,
        var priorityName: String? = null,
        var priorityColor: String? = null,


    var responseTime: Int? = null,

    var resolveTime: Int? = null,

    var handleTime: Int? = null,

    var dateAdded: Int? = null,
    var datePublished: Int? = null


) : AbstractJpaPersistable<Long>()


@MappedSuperclass
abstract class AbstractJpaPersistable<T: Serializable> : Persistable<T> {

    companion object {
        var serialVersionUID = -5554308939380869754L
    }
    abstract var pk_id: T?

    override fun getId() : T? {
        return pk_id
    }


    override fun isNew() = null == getId()

    override fun toString() ="Entity oftype  ${this.javaClass.name} with id: $id"

    override fun equals(other: Any?): Boolean {
        other ?: return false

        if (this === other) return true

        if (javaClass != ProxyUtils.getUserClass(other)) return false

        other as AbstractJpaPersistable<*>

        return if (null == this.getId()) false else this.getId() == other.getId()
    }


    override fun hashCode(): Int {
        return 58
    }


}

fun deserializeMentions(body: String) {
            var export = CxExport()
            var response = mapper.readTree(body)
            var count = response["response"]["count"].asInt()
            export.exportCount = count
            export.accountId= 51216
            export.exportType = ExportType.mention
            export.exportStart = Timestamp.from(Instant.now())
            export.exportEnd = Timestamp.from(Instant.now())
            cxExportRepo.saveAndFlush(export)

            var mentions: MutableList<Mention> = ArrayList()

            var data = response["response"]["data"]


            data.forEach {
                println(it["permalink"]?.asText())

                var locContinent: String? = null
                var locCountry: String? = null
                var locCity: String? = null
                var locRegion: String? = null
                var locLongitude: String? = null
                var locLatitude: String? = null

                var priorityId: Int? = null
                var priorityName: String? = null
                var priorityColor: String? = null

                it["priority"]?.let {
                    priorityId = it["id"]?.asInt()
                    priorityColor = it["color"]?.asText()
                    priorityName = it["name"]?.asText()
                }


                it["location"]?.let {
                    locContinent = it["continent"]?.asText()
                    locCountry = it["country"]?.asText()
                    locCity = it["city"]?.asText()
                    locRegion = it["region"]?.asText()
                    locLongitude = it["longitude"]?.asText()
                    locLatitude = it["latitude"]?.asText()

                }

                var tags: String? = null
                it["author"]["tags"]?.forEach {
                    it?.asText()?.let {
                        tags += it
                    }
                }
                mentions.add(Mention(
                        cxExport = export,
                        authorId = it["author"]["id"]?.asText(),
                        authorUrl = it["author"]["url"]?.asText(),
                        authorImg = it["author"]["img"]?.asText(),
                        authorTags = tags,
                        messageTitle =it["message"]["title"]?.asText(),
                        messageContent =it["message"]["content"]?.asText(),
                        messageLanguage = it["message"]["language"]?.asText(),
                        messageSentiment =it["message"]["sentiment"]?.asText(),
                        sourceId = it["source"]["id"]?.asText(),
                        sourceCategory = it["source"]["category"]?.asText(),
                        sourceType = it["source"]["type"]?.asText(),
                        sourceDomain = it["source"]["domain"]?.asText(),
                        sourceUrl = it["source"]["url"]?.asText(),
                        sourceProfile = it["source"]["profile_id"]?.asText(),
                        sourceProfileName = it["source"]["profile_name"]?.asText(),
                        locContinent = locContinent,
                        locCountry = locCountry,
                        locCity = locCity,
                        locRegion = locRegion,
                        locLongitude = locLongitude,
                        locLatitude = locLatitude,
                        permalink = it["permalink"]?.asText(),
                        priorityId = priorityId,
                        priorityName = priorityName,
                        priorityColor = priorityColor,
                        responseTime = it["timestamps"]["response_time"]?.asInt(),
                        resolveTime = it["timestamps"]["resolve_time"]?.asInt(),
                        handleTime = it["timestamps"]["handle_time"]?.asInt(),
                        dateAdded = it["date"]["added"]?.asInt(),
                        datePublished = it["date"]["published"]?.asInt()
                )
                )




            }

            mentionRepo.saveAll(mentions)




        }
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I'm about kotlin, not about kotson or other kotlin-json libraries.
Also, when writing, I jumped into reification. This isn't really important...

Reified Inline Extension functions

The only reason I'm writing this is because I'm using extension-functions later on and I don't know your kotlin-level. You can skip this if you understand.

You can skip reified completely.

extension function

Extension-functions are functions which make it look like you add functions to an existing class. An example:

fun <T> Any.cast() : T= this as T

As you can see, we refer to the receiver (Any) with this.

override fun equals(other : Any?) : Boolean{
    ...
    val otherId = other.cast<AbstractJpaPersistable<*>()
        .getId() ?: return false
    ...
}

This function makes the code more flowable.

Inline

Inline means that kotlin will replace the call to a function with the body of the function.

inline fun sayHi() = println("hi")
fun main() = sayHi()
//will be replaced with
fun main() = println("hi")

reified

Just to make inline extension functions interesting, we can write the safeCase-function:

fun <T> safeCast() : T? = when(this){
    is T -> this // exception
    else -> null
}

At runTime, the generics are erased and become Any? This means the former function becomes:

fun safeCast() : T? = when(this){
    is null -> this // exception
    else -> null
}

As we know, kotlin can create extra code at compiletime by inlining code.
We also know that the passed generics are know at compiletime.
When we combine those things, we can ask kotlin to create code, whereby it replaces the generics with the types it knows the generics represent.
We do this by adding reified to the generic:

inline fun <reified T> Any.safeCast() : T? = when(this){
    is T -> this
    else -> null
}

override fun equals(other : Any?) : Boolean{
    ...
    val otherId = other.safeCast<AbstractJpaPersistable<*>()
        ?.getId() ?: return false
    ...
}

at runtime, this will become:

override fun equals(other : Any?) : Boolean{
    ...
    val otherId = when(other){
        is AbstractJpaPersistable -> other 
        else -> null
    }?.getId() ?: return false
    ...
}

Because kotlin inlines the functions, it knows what the types are from parameters that go in and what come out.
Therefor, it's possible to rewrite the normal cast a bit safer using castOrThrow with or without generic param:

inline fun <reified T> Any.castOrThrow() : T{
     return when(this){
          is T -> this
          else -> throw ClassCastException("")
     }
}

fun main(){
    val a : Any = 5.castOrThrow()
    val a = 5.castOrThrow<Int>()
    val string : String = 5.castOrThrow() //throws
}

scope functions

I saw you using let:

  • reference to the receiver using it
  • returns result of lambda
  • receiver.let{ it.toString() }

You also have also:

  • reference to the receiver using it
  • returns the receiver
  • receiver.also{ it.doSomething() }

You can use this for creating CxExport:

val export = CxExport().also {
    it.exportCount = count
    it.accountId = 51216
    it.exportType = ExportType.mention
    val now = Timestamp.from(Instant.now())
    it.exportStart = now
    it.exportEnd = now
}

But... we have 2 more:

run:

  • reference to receiver using this:
  • returns result of lambda
  • receiver.run{ this.toString() }

And apply:

  • reference to receiver using this
  • returns the receiver
  • receiver.apply{ this.doSomething() }

And as this doesn't have to be written: receiver.apply{ doSomething() }
this means the code can be rewritten as:

val export = CxExport().apply {
    exportCount = count
    accountId = 51216
    exportType = ExportType.mention
    val now = Timestamp.from(Instant.now())
    exportStart = now
    exportEnd = now
}

inline classes

Kotlin has inline classes (they work roughly the same as Integer/int in Java).
You can create a inline class which takes one argument and add functions to that argument.
The inlined class itself is not used (if you call it from kotlin and the type is known):

When inline classes are boxed:

  • When using generics
  • Inline classes can implement interfaces. Passing it where interface is expected will box
  • When making type nullable

I believe this is the perfect way to make the data.foreach better

inline class Location(private val value : Map<String, String?>?){
    operator fun Map<String, String?>?.get(key: String) = this?.get(key)
    val continent get() = value["continent"]?.asText()
    val country get() = value["country"]?.asText()
    val city get() = value["city"]?.asText()
    val region get() = value["region"]?.asText()
    val longitude get() = value["longitude"]?.asText()
    val latitude get() = value["latitude"]?.asText()
}

inline class Priority(private val value : Map<String, String?>?){
    operator fun Map<String, String?>?.get(key: String) = this?.get(key)
    val id get() = value["id"]?.asInt()
    val name get() = value["name"]?.asText()
    val color get() = value["color"]?.asText()
}

As i said before, making the inline class nullable, means that the class is not inlined anymore.
Because you have to call value?.get("id") on a nullable map, I created an extension-function on the nullable Map.

We can place this function in an interface as well.

inline class Location(private val value : Map<String, String?>?) : NullableMapGetter{
    val continent get() = value["continent"]?.asText()
    val country get() = value["country"]?.asText()
    val city get() = value["city"]?.asText()
    val region get() = value["region"]?.asText()
    val longitude get() = value["longitude"]?.asText()
    val latitude get() = value["latitude"]?.asText()
}

interface NullableMapGetter{
    operator fun Map<String, String?>?.get(key: String) = this?.get(key)
}

inline class Priority(private val value : Map<String, String?>?) : NullableMapGetter{
    val id get() = value["id"]?.asInt()
    val name get() = value["name"]?.asText()
    val color get() = value["color"]?.asText()
}

And of course, you should use them:

data.forEach {
    println(it["permalink"]?.asText())

    val location = Location(it["location"])
    val priority = Priority(it["priority"])
    createMention(location, priority)
}

fun createMention(
    location : Location,
    priority : Priority
) = Mention(
        authorTags = authorTags,
        locContinent = location.continent,
        locCountry = location.country,
        locCity = location.city,
        locRegion = location.region,
        locLongitude = location.longitude,
        locLatitude = location.latitude,
        priorityId = priority.id,
        priorityName = priority.name,
        priorityColor = priority.color,
)

Collection functions

Don't know if it's possible, as most of the code doesn't compile as eg. mapper-declaration is absent. (code has to compile on code-review, so this question is actually of topic atm).

map

However, data.forEach does nothing else then mapping, so:

val mentions = mutableListOf<Mention>()
val data = response["response"]["data"]
data.forEach{
     //val newType = changeType(it)
     mensions.add(newType)
}

can be rewritten to:

val mentions = response["response"]["data"].map{
     changeType(it)
     // or return@map changeType(it)
}

joinToString

Kotlin has buildin functions to join a collection to a string: it["author"]["tags"]?.forEach { it?.asText()?.let { tags += it } }

can be rewritten as:

val tags = it["author"]["tags"].joinToString(""){
    it?.asText()
}
\$\endgroup\$
5
  • 1
    \$\begingroup\$ Thanks for the informative post, that's a lot to digest... When I use .map instead of forEach It fails when I try to persist it as it can't infer the type for some reason. Any idea why? I debugged and it is still an ArrayList<Mention> object after the .map body... \$\endgroup\$ Commented Feb 19, 2020 at 14:55
  • \$\begingroup\$ What happens if you provide the generics? \$\endgroup\$ Commented Feb 19, 2020 at 15:06
  • \$\begingroup\$ I tried many things but it kept complaining that it expects a type of (Itterable)<Mention>... for now I am back to the forEach as I had to get a sample of the data in the DB before tomorrow but would like to spend some time figuring this out still... I even tried .map{ }.toMutableList().asItterable ut no luck... \$\endgroup\$ Commented Feb 19, 2020 at 17:52
  • \$\begingroup\$ What did you mean by changeType(it) // or return@map changeType(it) in the .map body? I just had .map { Mention(/*constructor values)*/) } When I debugged I saw that it was a List<Mention> but it still didn't compile with the error being on the line where I call mentionRepo.saveAndFlush \$\endgroup\$ Commented Feb 19, 2020 at 18:16
  • \$\begingroup\$ What is the function signature of saveAndFlush? \$\endgroup\$ Commented Feb 19, 2020 at 20:21

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.