Does the persistence framework support Polymorphic Deserialization ?

Answered

I'm trying to implement persistence for a hierarchical tree for bookmarks. Initially my implementation had an abstract base class for the "directories" and "bookmarks". This was serialising correctly but wouldn't deserialise (it would not give a sensible error, null pointer exceptions). So I ported to interface as base with data classes as implementations, serialisation was the same but I couldn't deserialise (this time I got more sensible error messages). I then switched to the directories having two lists, one for the "subdirectories" and one for the "bookmarks".

So, here are my questions:

1. Can I have a polymorphic hierarchy or do I need to stick to manual tie breaking (I have to be doing something wrong) ? 

2. Must I implement Comparable ? 

3. Any other tips, new to kotlin ? 

package blast.browser.components


import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.annotations.AbstractCollection
import com.intellij.util.xmlb.annotations.Attribute
import com.intellij.util.xmlb.annotations.Tag
import java.util.*
import kotlin.comparisons.compareValuesBy

interface BookmarkNode : TreeElement {
var displayName: String
var id: String
var parent: String
}

data class BookmarkDirectory(
@Attribute override var displayName: String = "",
@Attribute override var id: String = UUID.randomUUID().toString(),
@Attribute override var parent: String = "",
@Tag @AbstractCollection(surroundWithTag = false) var directories: java.util.HashSet<BookmarkDirectory> = java.util.HashSet<BookmarkDirectory>(),
@Tag @AbstractCollection(surroundWithTag = false) var bookmarks: java.util.HashSet<Bookmark> = java.util.HashSet<Bookmark>()
) : BookmarkNode, Comparable<BookmarkDirectory> {
override fun compareTo(other: BookmarkDirectory): Int = compareValuesBy(this, other, {it.id})

override fun getPresentation(): ItemPresentation {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun getChildren(): Array<out TreeElement> {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

data class Bookmark(
@Attribute override var displayName: String = "",
@Attribute var url: String = "",
@Attribute override var id: String = UUID.randomUUID().toString(),
@Attribute override var parent: String = ""
) : BookmarkNode, Comparable<Bookmark> {
override fun compareTo(other: Bookmark): Int = compareValuesBy(this, other, {it.id})

override fun getPresentation(): ItemPresentation {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun getChildren(): Array<out TreeElement> {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

interface BookmarkManager {
fun addNode(parent: BookmarkDirectory, node: BookmarkNode)
fun removeNode(parent: BookmarkDirectory, node: BookmarkNode)
}


@State(name = "bookmarks", storages = arrayOf(Storage("bookmark.xml")))
class BookmarkManagerImpl : PersistentStateComponent<BookmarkDirectory>, BookmarkManager {
val root: BookmarkDirectory
private var nodes: MutableMap<String, BookmarkNode> = mutableMapOf()

init {
println("init")
root = BookmarkDirectory()
root.displayName = "root"
root.id = "root"
root.parent = ""
nodes.put("root", root)

// val bmm = Bookmark("bang", "boom")
// addNode(root, bmm)
//
// val bmd = BookmarkDirectory("badboing")
// addNode(bmd, Bookmark("badonk", "baboom"))
//
// addNode(root, bmd)
// println("done init")
}

override fun addNode(parent: BookmarkDirectory, node: BookmarkNode) {
when(node) {
is Bookmark -> parent.bookmarks.add(node)
is BookmarkDirectory -> parent.directories.add(node)
}
node.parent = parent.id
nodes.put(node.id, node)
}

override fun removeNode(parent: BookmarkDirectory, node: BookmarkNode) {
when(node) {
is Bookmark -> parent.bookmarks.remove(node)
is BookmarkDirectory -> parent.directories.remove(node)
}
node.parent = ""
nodes.remove(node.id)
}

override fun getState(): BookmarkDirectory {
return root
}

    override fun loadState(state: BookmarkDirectory?) {
state!!.bookmarks.map({nodes.put(it.id, it)})
state.directories.map({loadState(it)})
}

}

 

 

0
3 comments

previous implementation looked something like this : 


package blast.browser.components


import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.annotations.AbstractCollection
import com.intellij.util.xmlb.annotations.Attribute
import com.intellij.util.xmlb.annotations.Tag
import java.util.*
import kotlin.comparisons.compareValuesBy

abstract class BookmarkNode(
@Attribute var displayName: String,
@Attribute var id: String = UUID.randomUUID().toString(),
@Attribute var parent: String? = ""
) : TreeElement, Comparable<BookmarkNode> {
override fun compareTo(other: BookmarkNode): Int = compareValuesBy(this, other, { it.id })
}

class BookmarkDirectory(
displayName: String = "",
@Tag @AbstractCollection(surroundWithTag = false) var nodes: MutableSet<BookmarkNode> = mutableSetOf<BookmarkNode>()
) : BookmarkNode(displayName) {
override fun getPresentation(): ItemPresentation {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun getChildren(): Array<out TreeElement> {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

class Bookmark(
displayName: String = "",
@Attribute var url: String = "") : BookmarkNode(displayName) {
override fun getPresentation(): ItemPresentation {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}

override fun getChildren(): Array<out TreeElement> {
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

interface BookmarkManager {
fun addNode(parent: BookmarkDirectory, node: BookmarkNode)
fun removeNode(parent: BookmarkDirectory, node: BookmarkNode)
}


@State(name = "bookmarks", storages = arrayOf(Storage("bookmark.xml")))
class BookmarkManagerImpl : PersistentStateComponent<BookmarkDirectory>, BookmarkManager {
var root: BookmarkDirectory? = null
private var nodes: MutableMap<String, BookmarkNode> = mutableMapOf()

init {
initRoot()

val bmm = Bookmark("bang", "boom")
addNode(root!!, bmm)

val bmd = BookmarkDirectory("badboing")
addNode(bmd, Bookmark("badonk", "baboom"))

addNode(root!!, bmd)
}

override fun addNode(parent: BookmarkDirectory, node: BookmarkNode) {
parent.nodes.add(node)
node.parent = parent.id
nodes.put(node.id, node)
}

override fun removeNode(parent: BookmarkDirectory, node: BookmarkNode) {
parent.nodes.remove(node)
node.parent = null
nodes.remove(node.id)
}

override fun getState(): BookmarkDirectory {
println("getState")
return root!!
}

override fun loadState(state: BookmarkDirectory?) {
root = state!!
consume(root!!, nodes)
}

private fun initRoot() {
root = BookmarkDirectory()
root!!.displayName = "root"
root!!.id = "root"
root!!.parent = null
nodes.put("root", root!!)
}

private fun consume(bmd: BookmarkNode, target: MutableMap<String, BookmarkNode>) {
when(bmd) {
is Bookmark -> target.put(bmd.id, bmd)
is BookmarkDirectory -> {
target.put(bmd.id, bmd)
bmd.nodes.map({ consume(it, target) })
}
}
}
}
0

1. Yes, it's possible to use polymorphic collection, but you need to specify all implementation classes explicitly in 'elementTypes' attributes of @AbstractCollection

2. No, Comparable isn't required for serialization (though you may sort the elements in collections in your code to ensure that their order won't be changed in xml configuration files).

3. We don't have special tips for Kotlin. 'var' properties in Kotlin produces getters and setters in class-files which are properly used by the serializer.

0

Aha that did the trick. Perhaps you could add snippets to the official plugin documentation. Examples in the codebase are either very simple or very complicated. Another thing that tripped me up (hour or two timesink) was the storage locations, I didn't realise that it's a combination of the @Storage annotation + which scope you register the service / component under. 

Here is the updated code: 

package blast.browser.components


import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.annotations.AbstractCollection
import com.intellij.util.xmlb.annotations.Attribute
import com.intellij.util.xmlb.annotations.Tag
import java.util.*
import kotlin.comparisons.compareValuesBy

abstract class BookmarkNode(
@Attribute var displayName: String,
@Attribute var id: String = UUID.randomUUID().toString(),
@Attribute var parent: String? = ""
) : Comparable<BookmarkNode> {
override fun compareTo(other: BookmarkNode): Int = compareValuesBy(this, other, { it.id })
}

class BookmarkDirectory(
displayName: String = "",
@Tag @AbstractCollection(surroundWithTag = false, elementTypes = arrayOf(BookmarkDirectory::class, Bookmark::class))
var nodes: java.util.HashSet<BookmarkNode> = java.util.HashSet<BookmarkNode>()
) : BookmarkNode(displayName) {}

class Bookmark(
displayName: String = "",
@Attribute var url: String = "") : BookmarkNode(displayName) {}

interface BookmarkManager {
fun addNode(parent: BookmarkDirectory, node: BookmarkNode)
fun removeNode(node: BookmarkNode)
}

@State(name = "bookmarks", storages = arrayOf(Storage("bookmark.xml")))
class BookmarkManagerImpl : PersistentStateComponent<BookmarkDirectory>, BookmarkManager {
val root: BookmarkDirectory
private var nodes: MutableMap<String, BookmarkNode> = mutableMapOf()

init {
root = BookmarkDirectory()
root.displayName = "root"
root.id = "root"
root.parent = null
nodes.put("root", root)
}

override fun addNode(parent: BookmarkDirectory, node: BookmarkNode) {
parent.nodes.add(node)
node.parent = parent.id
nodes.put(node.id, node)
}

override fun removeNode(node: BookmarkNode) {
(nodes.get(node.id) as BookmarkDirectory).nodes.remove(node)
node.parent = null
nodes.remove(node.id)
}

override fun getState(): BookmarkDirectory {
return root
}

override fun loadState(state: BookmarkDirectory?) {
root.nodes = state!!.nodes
consume(root, nodes)
}

private fun consume(bmd: BookmarkNode, target: MutableMap<String, BookmarkNode>) {
target.put(bmd.id, bmd)
when (bmd) { is BookmarkDirectory -> bmd.nodes.map({ consume(it, target) })
}
}
}
0

Please sign in to leave a comment.