Spring MongoDB - use abstract/sealed class field - java

sealed class Entity
data class Bacteria(
val uid: String,
val rank: String,
val division: String,
val scientificname: String,
val commonname: String
): Entity()
data class CTDDisease(
val diseaseId: String,
val name: String,
val altDiseaseIds: List<String>,
val parentIds: List<String>,
val definition: String?
) : Entity()
And then I want to define my document as
#Document(collection = "annotations")
data class Annotation(
#Id val id: String,
...
val spans: List<AnnotationSpan>
)
data class AnnotationSpan(
val startIndex: Int,
val endIndex: Int,
val entity: Entity? // Can be Bacteria or Disease or null
)
I also accept these 2 classes within the RequestBody from the client from time to time e.g.
#PutMapping("/annotations")
fun submitAnnotations(#RequestBody submission: Submissions): Mono<Void> { ... }
data class Submission(val annotations: List<AnnotationSpan>, ...) // AnnotationSpan contains Entity
but I get
java.lang.IllegalAccessException: class kotlin.reflect.jvm.internal.calls.CallerImpl$Constructor cannot access a member of class com.package.name.Entity with modifiers "private"
If I change the class Entity to an abstract class
abstract class Entity
then I don't get an error but my query operations keep going on forever.
Bacteria and Disease both have different fields so they should be distinguishable.
I tried using a hacky converter
#ReadingConverter
class NormalizedEntityReaderConverter: Converter<DBObject, NormalizedEntity> {
override fun convert(source: DBObject): NormalizedEntity? {
println(source.toString())
val gson = Gson()
return gson.runCatching { fromJson(source.toString(), CTDDisease::class.java) }.getOrNull()
?: gson.runCatching { fromJson(source.toString(), Bacteria::class.java) }.getOrNull()
}
}
and then registering it like
#Configuration
class MongoConverterConfig {
#Bean
fun customConversions(): MongoCustomConversions {
val normalizedEntityReaderConverter = NormalizedEntityReaderConverter()
val converterList = listOf<Converter<*, *>>(normalizedEntityReaderConverter)
return MongoCustomConversions(converterList)
}
}
My converter seems to work when called manually but for some reason, Spring still isn't picking it up.
I'm new to Spring. I would achieve this functionality in my Node.js server by using union types in TypeScript e.g.
interface AnnotationSpan {
startIndex: number;
endIndex: number;
entity?: Bacteria | Disease;
}
How can I achieve this behavior?

Related

Exception while computing database live data with Enum

I have added a data class and try to save it into Room. I went through stackoverflow and didn't find an answer.
So, the error is:
Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path $
Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path $
I am using Room 2.4.2 so enum is supposed to be supported.
The model I am using is :
#Entity(tableName = "userpreferencestable")
class UserPreferencesEntity (
#PrimaryKey()
var vin: String,
#ColumnInfo(name = "control")
var command: List<CommandTile?>
)
and CommandTile is defined as below:
data class CommandTile(
#SerializedName("name")
var name: DashboardTile.Name,
#SerializedName("state")
var state: DashboardTile.State
)
State and Name are enum and defined as below:
enum class Name {
NAME1,
NAME2...
}
enum class State {
TAT,
TOT
}
I have tried to add a DataConverter but it's not working.
#TypeConverter
fun fromName(name: Name): String {
return name.name
}
#TypeConverter
fun toName(name: String): Name {
return Name.valueOf(name)
}
#TypeConverter
fun fromState(state: State): String {
return state.name
}
#TypeConverter
fun toState(state: String):State {
return State.valueOf(state)
}
It still not working. I cannot figure out how to save the List of data class with enum.
Any idea ?
You issue is not the Enums, rather it is with the command List<CommandTile> (according to the disclosed code).
TypeConverters are for converting from/to a column for the data to be stored/retrieved.
As you have no #Entity annotation for the CommandTile class BUT instead have List<CommandTile?> as a column in the UserPrefrences class, which does have #Entity annotation, then Room will want to convert the List of CommandTiles to an acceptable type (in SQLite along with Room's restrictions this would have to be a type that resolves to one of TEXT (String), INTEGER (Int, Long ...), REAL (Double, Float ....) or BLOB (ByteArray).
types in parenthesise are Kotlin Types, they are examples and are not fully comprehensive.
As an example, overcoming issue that you may encounter using List, consider the following:-
A new class CommandTileList
data class CommandTileList(
val commandTileList: List<CommandTile>
)
to avoid a column that is a List
A modified UserPreferencesEntity class to use a CommandTileList rather than List<CommandTile>
#Entity(tableName = "userpreferencestable")
class UserPreferencesEntity (
#PrimaryKey()
var vin: String,
#ColumnInfo(name = "control")
var command: CommandTileList
)
the TypeConverters class, with appropriate TypeConverters
class TypeConverters {
#TypeConverter
fun fromCommandTileToString(commandTileList: CommandTileList): String {
return Gson().toJson(commandTileList)
}
#TypeConverter
fun fromStringToCommandTile(string: String): CommandTileList {
return Gson().fromJson(string,CommandTileList::class.java)
}
}
A suitable #Dao annotated class AllDAO (for demo)
#Dao
interface AllDAO {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(userPreferencesEntity: UserPreferencesEntity): Long
#Query("SELECT * FROM userpreferencestable")
fun getAllUserPreferences(): List<UserPreferencesEntity>
}
A suitable #Database annotated class TheDatabase (for demo) noting the TypeConverters class being defined with full scope via the #TypeConverters annotation (not the plural rather than singular form)
#TypeConverters(value = [TypeConverters::class])
#Database(entities = [UserPreferencesEntity::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDAO(): AllDAO
companion object {
var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance == null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
.allowMainThreadQueries for convenience/brevity
Finally putting it all into action witin an Activty MainActivity
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val c1 = CommandTile(DashboardTile.Name.NAME1,DashboardTile.State.TAT)
val c2 = CommandTile(DashboardTile.Name.NAME2,DashboardTile.State.TOT)
db = TheDatabase.getInstance(this)
dao = db.getAllDAO()
dao.insert(userPreferencesEntity = UserPreferencesEntity("VIN1", CommandTileList(listOf(
c1,c2))))
for(u in dao.getAllUserPreferences()) {
Log.d("DBINFO","VIV = ${u.vin} CommandTiles in Command = ${u.command.commandTileList.size} They Are:-")
for (ct in u.command.commandTileList) {
Log.d("DBINFO","\tName = ${ct.name} State = ${ct.state}")
}
}
}
}
Result
The log includes:-
D/DBINFO: VIV = VIN1 CommandTiles in Command = 2 They Are:-
D/DBINFO: Name = NAME1 State = TAT
D/DBINFO: Name = NAME2 State = TOT
The Database, via App Inspection :-
As you can see the list of 2 CommandTiles (a CommandTileList) has been converted to a String (SQLite type TEXT) and stored and subsequently retrieved.
Note from a database perspective this is not ideal it
limits/complicates the usefulness of the stored data.
For example (simple) if you wanted to select all States that start with A then SELECT * FROM userpreferencestable WHERE command LIKE 'A%' would find all columns you would have to use some like SELECT * FROM userpreferencestable WHERE 'state":A%'.
introduces bloat with all that extra data and thus introduces inefficiencies.
breaks normalisation as the same values are stored multiple times
The database way would be to have a table based upon the CommandTile incorporating the suitable relationship between CommandTiles and the UserPreferences.
Then there would be no need for the TypeConverter.
To convert enum value you have to do something like this
#TypeConverter
fun fromName(name: Name): String {
return name.name
}
#TypeConverter
fun toName(name: String): Name {
return enumValueOf<Name>(name)
}

Spring Data DynamoDB - Respository performing scan instead of query

Expected Behavior
Repository perform a query over a method that only uses hashKey and rangeKey attributes, and result this:
"{"TableName":"music","KeyConditionExpression":"artist = :_artist and begins_with(title, :_title)","ExpressionAttributeValues":{":_artist":{"S":"Djavan"},":_title":{"S":"Eu te devoro"}}}"
Actual Behavior
Repository perform a scanFilter over a method that only uses hashKey and rangeKey attributes, and result this:
"{"TableName":"music","ScanFilter":{"artist":{"AttributeValueList":[{"S":"Djavan"}],"ComparisonOperator":"EQ"},"title":{"AttributeValueList":[{"S":"Eu te devoro"}],"ComparisonOperator":"BEGINS_WITH"}}}"
Steps to Reproduce the Problem
Using a entity named Music
#DynamoDBTable(tableName = "music")
data class Music(
#field:Id
#DynamoDBIgnore
val id: MusicId = MusicId(),
var genre: String? = null
) {
#DynamoDBHashKey(attributeName = "artist")
fun getArtist() = id.artist
fun setArtist(artist: String) {
id.artist = artist
}
#DynamoDBHashKey(attributeName = "title")
fun getTitle() = id.title
fun setTitle(title: String) {
id.title = title
}
}
#DynamoDBDocument
data class MusicId(
#field:DynamoDBHashKey(attributeName = "artist")
var artist: String? = null,
#field:DynamoDBRangeKey(attributeName = "title")
var title: String? = null
) : Serializable
And a repository
#EnableScan //I know that if I remove this annotation, enables won't be permitted, but the problem is that the implementation code doesn't recognize my method as a key query and if I remove this annotation, the method falls on invocation
interface MusicRepository : CrudRepository<Music, MusicId> {
fun findByArtistAndTitleStartingWith(artista: String, sortKey: String): List<Music>
}
And when i invoke:
#PostConstruct
fun init() {
println(musicRepository.findByArtistAndTitleStartingWith("Djavan", "Eu te devoro").joinToString())
}
the log show's me the call to AWS as i showed above
Specifications
Lib: https://github.com/boostchicken/spring-data-dynamodb
Spring Data DynamoDB Version: 5.2.5
Spring Data Version: Doesn't used
Spring Boot Starter Web Version: 2.3.4.RELEASE
AWS SDK Version: 1.11.573
Java Version: 11
Platform Details: Windows
Github issue
did I something wrong? Or is the other approach that spring data create the correct query to aws?
After trying random changes in entity. I got the expected result in the repository method.
Correct way to map a Entity with composite key
#DynamoDBTable(tableName = "music")
data class Music(
#get:DynamoDBHashKey(attributeName = "artist")
var artist: String? = null,
#get:DynamoDBRangeKey(attributeName = "title")
var title: String? = null,
var genre: String? = null
) {
#Id
private var id: MusicId? = null
get() = MusicId(artist, title)
}
#DynamoDBDocument
data class MusicId(
#field:DynamoDBHashKey(attributeName = "artist")
var artist: String? = null,
#field:DynamoDBRangeKey(attributeName = "title")
var title: String? = null
) : Serializable
After change the entity and invoke:
musicRepository.findByArtistAndTitleStartingWith("Djavan", "Eu te devoro")
I got the corret invocation of AWS api.
"{"TableName":"music","ConsistentRead":true,"KeyConditions":{"artist":{"AttributeValueList":[{"S":"Djavan"}],"ComparisonOperator":"EQ"},"title":{"AttributeValueList":[{"S":"Eu te devoro"}],"ComparisonOperator":"BEGINS_WITH"}},"ScanIndexForward":true}"

Spring Data Mongo: Persisting Maps with non-simple Keys

I have a model
data class SomeComplexObject<T>(val name: String, val value: T)
#Document("model")
data class SomeModel<T>(val id: String? = null, val map: Map<SomeComplexObject<T>, Int>)
Which I save to Mongo via the save method of:
#Repository
interface SomeRepo<T>: MongoRepository<SomeModel<T>, String>
On it's own, this would throw a MappingException: Cannot use a complex object as a key value. error so i'm trying to find a work around using Converters where my Converters change the Map to a List before persisting and back from List -> Map on Read.
#WritingConverter
class Map2ListConverter: Converter<Map<SomeComplexObject<*>, Int>, List<Pair<SomeComplexObject<*>, Int>>> {
override fun convert(domainModel: Map<SomeComplexObject<*>, Int>): List<Pair<SomeComplexObject<*>, Int>> {
return domainModel.map { (key, value) -> Pair(key, value) }
}
}
#ReadingConverter
class List2MapConverter: Converter<List<Pair<SomeComplexObject<*>, Int>>, Map<SomeComplexObject<*>, Int>> {
override fun convert(dbModel: List<Pair<SomeComplexObject<*>, Int>>): Map<SomeComplexObject<*>, Int> {
return dbModel.toMap()
}
}
Which I register with
#Bean
fun customConversions(): MongoCustomConversions {
val converters = ArrayList<Converter<*, *>>()
converters.add(Map2ListConverter())
converters.add(List2MapConverter())
return MongoCustomConversions(converters)
}
This however does not work & I get a CodecConfigurationException: Can't find a codec for class kotlin.Pair.. It looks like spring tries to send a Document containing my Pair class direct to Mongo which it understandably doesn't know what to do with.
Is there a way around this? Or do I need to admit defeat and just store my Maps as Sets everywhere?

Unable to map application properties to object with dynamic property key

I am trying to get application property object by value, i already did this in Java, but from some reason using Kotlin i can not manage to do it.
So basically what i have is list of application properties that looks like this:
ee.car.config.audi.serial=1
ee.car.config.audi.base=platform1
ee.car.config.bmw.serial=2
ee.car.config.bmw.base=platform2
so as you can see car identifiers (audi,bmw,peugeot,etc..) are dynamic, and i need simply by serial value to get object that represents the specific car and by car key(audi, bmw) to get all other properties.
And what i did is simple, i created configuration properties class like this:
#Configuration
#ConfigurationProperties(prefix = "ee.car")
data class FesEngineKeys(
val config: HashMap<String, EeCarConfigParam> = HashMap()
) {
fun getOrDefaultEEConfig(engineKey: String): EeCarConfigParam? {
return config.getOrDefault(engineKey, config["audi"])
}
And then object to map keys after dynamic value:
data class EeCarConfigParam {
var serial: String,
var base: String
}
But problem here is, in FesEngineKeys class, config property is empty, it looks like EeCarConfigParam can not be mapped.
Also interesting part is when i change:
val config: HashMap<String, EeCarConfigParam> = HashMap() to
val config: HashMap<String, String> = HashMap()
then i can see that config param is populated with all the values.
This code already works in Java and it looks like this:
#Configuration
#Getter
#Setter
#ConfigurationProperties(prefix = "ee.car")
public class FESEngineKeys {
private Map<String, EeCarConfigParam> config = new HashMap<>();
public EeCarConfigParam getOrDefaultEEConfig(String engineKey) {
return config.getOrDefault(engineKey, config.get("audi"));
}
public EeCarConfigParam findBySerial(String serial) {
return config.values().stream().filter(cfg -> cfg.getSerial().equalsIgnoreCase(serial)).findFirst().orElse(null);
}
}
#Data
public class EeCarConfigParam {
private String serial;
private String base;
}
I really don't know why in the Kotlin case it is not working, i probably made some very basic mistake, and i would really appreciate if anyone can explain what is happening here
Okay i got it.
According to that: https://docs.spring.io/spring-boot/docs/2.0.x/reference/html/boot-features-kotlin.html the support for what you want is very limited.
I made it working like that - not pretty nice :-( :
#ConfigurationProperties(prefix = "ee.car")
class FesEngineKeyProperties() {
var config: MutableMap<String, EeCarConfigParam?>? = mutableMapOf()
fun getBase(serial: String): String {
if(config == null) return ""
return config!!["audi"]?.base ?: ""
}
}
class EeCarConfigParam() {
lateinit var serial: String
lateinit var base: String
}
#SpringBootApplication
#EnableConfigurationProperties(FesEngineKeyProperties::class)
class SandboxApplication
fun main(args: Array<String>) {
runApplication<SandboxApplication>(*args)
}
I was able to handle this issue, it is somehow related to kotlin, because once when i instead of this:
data class EeCarConfigParam {
var serial: String,
var base: String
}
used "norma" Java class, everything started working, so all code from my question stays the same, only difference is this: instead of Kotlin EeCardConfigParam i created Java class like this:
public class EeCarConfigParam {
private String publicUrl;
private String base;
}
Note: with all default getters, setters, equals, hash and toString methods.

How to register an InstanceCreator with Gson in Kotlin?

I can use Code 1 to save MutableList<MDetail> to json string using Gson correctly,
But I get the error when I try to restore MutableList<MDetail> object from json string with Code 2.
I have search some resources, it seems that I need to register an InstanceCreator.
How can I write a register an InstanceCreator code with Kotlin? Thanks!
Error
Caused by: java.lang.RuntimeException: Unable to invoke no-args constructor for interface model.DeviceDef. Registering an InstanceCreator with Gson for this type may fix this problem.
Code 1
private var listofMDetail: MutableList<MDetail>?=null
mJson = Gson().toJson(listofMDetail) //Save
Code 2
var mJson: String by PreferenceTool(this, getString(R.string.SavedJsonName) , "")
var aMListDetail= Gson().fromJson<MutableList<MDetail>>(mJson)
inline fun <reified T> Gson.fromJson(json: String) = this.fromJson<T>(json, object: TypeToken<T>() {}.type)
My Class
interface DeviceDef
data class BluetoothDef(val status:Boolean=false): DeviceDef
data class WiFiDef(val name:String, val status:Boolean=false) : DeviceDef
data class MDetail(val _id: Long, val deviceList: MutableList<DeviceDef>)
{
inline fun <reified T> getDevice(): T {
return deviceList.filterIsInstance(T::class.java).first()
}
}
Added
After I use val myGson = GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create(), I can get the correct result when I use open class DeviceDef , why?
open class DeviceDef
data class BluetoothDef(val status:Boolean=false): DeviceDef()
data class WiFiDef(val name:String, val status:Boolean=false) : DeviceDef()
val adapter = RuntimeTypeAdapterFactory
.of(DeviceDef::class.java)
.registerSubtype(BluetoothDef::class.java)
.registerSubtype(WiFiDef::class.java)
data class MDetail(val _id: Long, val deviceList: MutableList<DeviceDef>)
{
inline fun <reified T> getDevice(): T {
return deviceList.filterIsInstance(T::class.java).first()
}
}
val myGson = GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create()
Gson is having a hard time deserialising polymorphic objects as in your MutableList<DeviceDef>. Here's what you need to do:
Add the RuntimeTypeAdapterFactory.java to your project manually (does not seem to be part of gson library). See also this answer.
Change your code to use the factory
Create Gson instance:
val adapter = RuntimeTypeAdapterFactory
.of(DeviceDef::class.java)
.registerSubtype(BluetoothDef::class.java)
.registerSubtype(WiFiDef::class.java)
val gson = GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create()
register each of your subtypes in the factory and it will work as intended :)
maybe you have 'by lazy' initializing parameter or value in your data class
more information

Categories

Resources