How to design to reduce redundant classes in Kotlin? - java

I'm learning the sample "Kotlin for Android Developers (the book) " at https://github.com/antoniolg/Kotlin-for-Android-Developers
There are three classes in different kt file, I think the three classes are similar, and these mulit classes make the program complex.
How can I redesign the project framework and make project more clear?
DbClasses.kt
class DayForecast(var map: MutableMap<String, Any?>) {
var _id: Long by map
var date: Long by map
var description: String by map
var high: Int by map
var low: Int by map
var iconUrl: String by map
var cityId: Long by map
constructor(date: Long, description: String, high: Int, low: Int, iconUrl: String, cityId: Long)
: this(HashMap()) {
this.date = date
this.description = description
this.high = high
this.low = low
this.iconUrl = iconUrl
this.cityId = cityId
}
}
tables.kt
object DayForecastTable {
val NAME = "DayForecast"
val ID = "_id"
val DATE = "date"
val DESCRIPTION = "description"
val HIGH = "high"
val LOW = "low"
val ICON_URL = "iconUrl"
val CITY_ID = "cityId"
}
DomainClasses.kt
data class Forecast(
val id: Long,
val date: Long,
val description: String,
val high: Int,
val low: Int,
val iconUrl: String
)

In terms of database related classes, you may think about using ORM Library that will annotate fields and generate database table schema (remove need for DayForecastTable) e.g. Room (https://developer.android.com/training/data-storage/room/index.html)
You could technically use those classes across your whole app to reduce the need for DomainClasses, although I would suggest keeping Domain layer classes to make domain model independent from database.

Related

#Query says it does not have the fields, although I have them in the query itself -> Kotlin Android Room

I got this code in my DAO:
#Query("select Conversation.*, User.* from Conversation join User on Conversation.createdBy = User.userUuid where conversationUuid = :conversationUuid")
fun selectAllForOverview(conversationUuid: UUID): LiveData<List<ConversationSelectAllForOverview>>
This is ConversationSelectAllForOverview
data class ConversationSelectAllForOverview(
#Embedded(prefix = "arg0")
val arg0: DbConversation,
#Embedded(prefix = "arg1")
val arg1: DbUser
)
I read that I needed to annotate my fields with a prefix to get rid of errors when they have common field names. I get this error and I don't know how I can remove it. I am 100% sure all the columns are available, since DbConversation and DbUser are just generated from the database. How can I fix this problem? DbConversation and DbUser share some columns, see the definition of DbConversation here: https://gist.github.com/Jasperav/381243e7b3cf387bfc0e9f1343f9faeb. DbUser looks the same.
error: The columns returned by the query does not have the fields
[conversationUuid,createdBy,tsCreated,distanceMapped,showOnMap,showOnOverview,allowMessagesByInRangeRegularUsers,allowMessagesByOutOfRangeRegularUsers,stillReadableForOutOfRangeRegularUsers,freedomInReplies,title,subject,likes,latitude,longitude,hasPassword,isSubscribed,showOnMapScreen,isLiked,bypassChecks,isHidden,nsfw,currentDirectEvents,totalDirectEventsAfterLastJoin,subscriptions,userUuid,username,karma,tsCreated,allowsPrivateChats,allowsNsfw,thisUserBlockedCurrentUser,incomingFriendshipRequest,outstandingFriendshipRequest,friends,bio,appRoleMapped]
in entity.ConversationSelectAllForOverview even though they are
annotated as non-null or primitive. Columns returned by the query:
[conversationUuid,createdBy,tsCreated,distanceMapped,showOnMap,showOnOverview,allowMessagesByInRangeRegularUsers,allowMessagesByOutOfRangeRegularUsers,stillReadableForOutOfRangeRegularUsers,freedomInReplies,title,subject,likes,avatar,latitude,longitude,hasPassword,isSubscribed,showOnMapScreen,isLiked,bypassChecks,isHidden,conversationReportReasonMapped,nsfw,currentDirectEvents,totalDirectEventsAfterLastJoin,lastReadConversationEventPk,mostRecentConversationEventUuid,relevance,subscriptions,userUuid,username,karma,tsCreated,allowsPrivateChats,allowsNsfw,avatar,currentUserBlockedThisUserTsCreated,thisUserBlockedCurrentUser,searchScreenScore,recentSearchedTsCreated,userReportReasonMapped,incomingFriendshipRequest,outstandingFriendshipRequest,friends,bio,appRoleMapped]
public abstract androidx.lifecycle.LiveData<java.util.List<entity.ConversationSelectAllForOverview>>
selectAllForOverview(#org.jetbrains.annotations.NotNull()
The Issue
You are prefixing the columns in the #Embedded annotation in conjunction with the output columns not being prefixed according to the query.
For example the ConversationSelectAllForOverview class is expecting to find the column named arg0conversationUuid in the output/result of the query but the query only has the column conversationUuid.
The Fix
Instead of using select Conversation.*, User.* .... you need to use
select Conversation.conversationUuid AS arg0conversationUuid, Conversation.createdBy AS arg0createdBy ....
AS giving the output conversationUuid column an alias of arg0conversationUuid etc.
i.e. for every column in both tables you have to alias the actual column with it's prefix.
e.g. using (only partially adjusted):-
#Query("select " +
"Conversation.conversationUuid AS arg0conversationUuid" +
", Conversation.createdBy AS arg0createdBy" +
", Conversation.tsCreated AS arg0tsCreated" +
", Conversation.distanceMapped AS arg0distanceMapped" +
/* .... */
", User.* from Conversation join User on Conversation.createdBy = User.userUuid where conversationUuid = :conversationUuid")
fun selectAllForOverview(conversationUuid: UUID): LiveData<List<ConversationSelectAllForOverview>>
the message is then The columns returned by the query does not have the fields [showOnMap,showOnOverview ....
i.e. conversationUuid, createdBy, tsCreated and distanceMapped are now not included in the field-column mismatch list.
Alternative Fix (untested and reliant upon Room 2.5.0 libraries )
Another solution, which may work with Room 2.5.0 (untested) would be to use the #Relation annotation instead of the #Embedded annotation for the child(ren). e.g. without any other changes other than:-
data class ConversationSelectAllForOverview(
#Embedded/*(prefix = "arg0")*/
val arg0: DbConversation,
#Relation(
entity = DbUser::class,
parentColumn = "createdBy",
entityColumn = "userUuid"
)
val arg1: DbUser
)
and then using :-
#Transaction
#Query("SELECT * FROM Conversation WHERE conversationUUid=:conversationUuid")
fun selectAllForOverview(conversationUuid: UUID): LiveData<List<ConversationSelectAllForOverview>>
Then it compiles successfully (again not run, see below). i.e. there are no issues with the duplicated columns (again see below).
You could also use (note the function name being different to allow both to be compiled):-
#Transaction
#Query("select Conversation.*,User.* from Conversation join User on Conversation.createdBy = User.userUuid where conversationUuid = :conversationUuid")
fun selectAllForOverviewAlt(conversationUuid: UUID): LiveData<List<ConversationSelectAllForOverview>>
However the JOINed columns are superfluous as Room uses the #Relationship parameters to then build the DbUser object via a subsequent query (hence the #Transaction).
NOTE Room used to take the last value found, for a like named output column, and apply that last value to all of the fields so named. This has reportedly been fixed with 2.5.0. This hasn't been confirmed as being the case though. As such you may get unintended results, so if you take this approach you should confirm that values are as expected (i.e. check the values of the like named columns/fields).
Additional a Demo
The following is a working demo based upon the code as per the question DbConversation class. BUT, to simplify matters other code has been made up and additionally many of the fields have been commented out. LiveData has been commented out and .allowMainThreadQueries to simplify the demo.
The Demo uses both fixes and for the #Relation both the original query and the suggested more consice query.
A debugging breakpoint has been used to demonstrate the sets of 3 returns.
The Database Code (everything needed for the demo plus some that isn't due to commenting out). It should be Noted that much of the code may differ, simple assumptions have been made.
:-
#Entity(
tableName = "Conversation",
primaryKeys = ["conversationUuid"],
indices = [/*Index(value = ["nsfw", "relevance"]), Index(value = ["isSubscribed"]),*/ Index(value = ["createdBy"])/*, Index(
value = ["avatar"]
), Index(value = ["mostRecentConversationEventUuid"])*/],
foreignKeys = [/*ForeignKey(
entity = DbConversationEventMostRecent::class,
childColumns = ["mostRecentConversationEventUuid"],
parentColumns = ["conversationEventUuid"],
onDelete = SET_NULL,
onUpdate = CASCADE,
), ForeignKey(
entity = DbMedia::class,
childColumns = ["avatar"],
parentColumns = ["mediaUuid"],
onDelete = CASCADE,
onUpdate = NO_ACTION,
), */ForeignKey(
entity = DbUser::class,
childColumns = ["createdBy"],
parentColumns = ["userUuid"],
onDelete = CASCADE,
onUpdate = NO_ACTION,
)]
)
data class DbConversation(
#ColumnInfo(typeAffinity = ColumnInfo.TEXT)
val conversationUuid: UUID,
#ColumnInfo(typeAffinity = ColumnInfo.TEXT)
val createdBy: UUID,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val tsCreated: Long,
#ColumnInfo(typeAffinity = ColumnInfo.TEXT)
val distanceMapped: ConversationDistance,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val showOnMap: Boolean,
/*
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val showOnOverview: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val allowMessagesByInRangeRegularUsers: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val allowMessagesByOutOfRangeRegularUsers: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val stillReadableForOutOfRangeRegularUsers: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val freedomInReplies: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.TEXT)
val title: String,
#ColumnInfo(typeAffinity = ColumnInfo.TEXT)
val subject: String,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val likes: Long,
#ColumnInfo(typeAffinity = ColumnInfo.TEXT)
val avatar: UUID?,
#ColumnInfo(typeAffinity = ColumnInfo.REAL)
val latitude: Double,
#ColumnInfo(typeAffinity = ColumnInfo.REAL)
val longitude: Double,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val hasPassword: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val isSubscribed: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val showOnMapScreen: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val isLiked: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val bypassChecks: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val isHidden: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.TEXT)
val conversationReportReasonMapped: ConversationReportReason?,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val nsfw: Boolean,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val currentDirectEvents: Long,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val totalDirectEventsAfterLastJoin: Long,
#ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val lastReadConversationEventPk: ConversationEventPk?,
#ColumnInfo(typeAffinity = ColumnInfo.TEXT)
val mostRecentConversationEventUuid: UUID?,
#ColumnInfo(typeAffinity = ColumnInfo.REAL)
val relevance: Double?,
#ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
val subscriptions: Long
*/
)
#Dao
interface AllDAOs {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(dbUser: DbUser): Long
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(dbConversation: DbConversation): Long
#Query("select " +
"Conversation.conversationUuid AS arg0conversationUuid" +
", Conversation.createdBy AS arg0createdBy" +
", Conversation.tsCreated AS arg0tsCreated" +
", Conversation.distanceMapped AS arg0distanceMapped" +
", Conversation.showOnMap AS arg0showOnMap" +
/* .... */
",User.userUuid AS arg1userUuid" + /*?????? made up/incomplete/asssumed */
",User.userName AS arg1userName" +
" from Conversation join User on Conversation.createdBy = User.userUuid where conversationUuid = :conversationUuid")
fun selectAllForOverviewOld(conversationUuid: UUID): /*LiveData<*/List<OldConversationSelectAllForOverview>/*>*/
#Query("SELECT * FROM Conversation WHERE conversationUuid=:conversationUuid")
fun selectConversationByUuid(conversationUuid: UUID): List<DbConversation>
#Query("SELECT * FROM Conversation WHERE conversationUuid=:conversationUuid")
fun selectAllForOverview(conversationUuid: UUID): /*LiveData<*/List<ConversationSelectAllForOverview>/*>*/
#Query("select Conversation.*,User.* from Conversation join User on Conversation.createdBy = User.userUuid where conversationUuid = :conversationUuid")
fun selectAllForOverviewAlt(conversationUuid: UUID): /*LiveData<*/List<ConversationSelectAllForOverview>/*>*/
}
data class ConversationDistance(
val blah: String
)
data class ConversationReportReason(
val blah: String
)
data class ConversationEventPk(
val blah: ByteArray
)
class RoomTypeConverters {
#TypeConverter
fun fromConversationDistanceToJSON(conversationDistance: ConversationDistance): String = Gson().toJson(conversationDistance)
#TypeConverter
fun toConversationDistanceFromJSON(json: String): ConversationDistance = Gson().fromJson(json,ConversationDistance::class.java)
#TypeConverter
fun fromConversationReportReasonToJSON(conversationReportReason: ConversationReportReason): String = Gson().toJson(conversationReportReason)
#TypeConverter
fun toConversationReportReasonFromJSON(json: String): ConversationReportReason = Gson().fromJson(json,ConversationReportReason::class.java)
#TypeConverter
fun fromConversationEventPkToByteArray(conversationEventPk: ConversationEventPk): ByteArray = ByteArray(100)
#TypeConverter
fun toConversationEventPkFromByteArray(byteArray: ByteArray): ConversationEventPk = ConversationEventPk(byteArray)
}
#Entity(tableName = "User")
data class DbUser(
#PrimaryKey
val userUuid: UUID,
val userName: String
)
#Entity
data class DbMedia(
#PrimaryKey
val mediaUuid: UUID,
val mediaName: String
)
#Entity
data class DbConversationEventMostRecent(
#PrimaryKey
val conversationEventUuid: UUID
)
data class ConversationSelectAllForOverview(
#Embedded/*(prefix = "arg0")*/
val arg0: DbConversation,
#Relation(
entity = DbUser::class,
parentColumn = "createdBy",
entityColumn = "userUuid"
)
val arg1: DbUser
)
data class OldConversationSelectAllForOverview(
#Embedded(prefix = "arg0")
val arg0: DbConversation,
#Embedded(prefix = "arg1")
val arg1: DbUser
)
#TypeConverters(value = [RoomTypeConverters::class])
#Database(entities = [DbMedia::class,DbUser::class,DbConversationEventMostRecent::class,DbConversation::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAllDAOs(): AllDAOs
companion object {
private var instance: TheDatabase? = null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance=Room.databaseBuilder(context,TheDatabase::class.java,"the_database")
.allowMainThreadQueries() /* For brevity of demo */
.build()
}
return instance as TheDatabase
}
}
}
The Activity Code:-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AllDAOs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAllDAOs()
val u1uuid = UUID(10L,10L)
val u2uuid = UUID(11L,12L)
val c1uuid = UUID(20L,20L)
val c2uuid = UUID(21L,21L)
val c3uuid = UUID(22L,10L)
val c4uuid = UUID(15L,10L)
dao.insert(DbUser(u1uuid,"Fred"))
dao.insert(DbUser(u2uuid,"Mary"))
dao.insert(DbConversation(c1uuid,u1uuid,0, ConversationDistance("blah blah blah"),true))
dao.insert(DbConversation(c2uuid,u2uuid,0,ConversationDistance("uhm uhm uhm"),true))
dao.insert(DbConversation(c3uuid,u1uuid,0,ConversationDistance("meh meh meh"),true))
dao.insert(DbConversation(c4uuid,u1uuid,0,ConversationDistance("good good good"),true))
val c1 = dao.selectConversationByUuid(c1uuid)
val c2 = dao.selectConversationByUuid(c2uuid)
val c3 = dao.selectConversationByUuid(c3uuid)
val c4 = dao.selectConversationByUuid(c4uuid)
val t1c1 = dao.selectAllForOverviewOld(c1uuid)
val t1c2 = dao.selectAllForOverview(c1uuid)
val t1c3 = dao.selectAllForOverviewAlt(c1uuid)
val t2c1 = dao.selectAllForOverviewOld(c2uuid)
val t2c2 = dao.selectAllForOverview(c2uuid)
val t2c3 = dao.selectAllForOverviewAlt(c2uuid)
if (t1c1==null) t1c1==null /*<<<<<<<<<< BREAKPOINT HERE >>>>>>>>>>*/
}
}
When run from a fresh install, then The debug window, with t1?? and t2?? expanded:-
As can be seen all 3 queries produce the same result. As such the simplest solution would be to
ensure that you are using Room 2.5.0 libraries, AND
use #Relation instead of #Embedded and use the more concise query just extracting the relevant DbConversation(s).
Take a look at your classes DbConversation and DbUser and make sure these classes are #Embeddable. If not, put the annotation on the class.
Another solution is look at the error again. It say that query return a bunch of columns that your DTO (ConversationSelectAllForOverview) doens't hold; the fields of your DTO are the objects and not the columns fields. You can create an interface receiving all those columns that db return:
interface nameInterface(
val conversationUuid: Any,
val createdBy: Any,
val tsCreated: Any,
val distanceMapped: Any,
val showOnMap: Any,
val showOnOverview: Any,
val allowMessagesByInRangeRegularUsers: Any,
val allowMessagesByOutOfRangeRegularUsers: Any,
val stillReadableForOutOfRangeRegularUsers: Any,
val freedomInReplies: Any,
val title: Any,
val subject: Any,
val likes: Any,
val latitude: Any,
val longitude: Any,
val hasPassword: Any,
val isSubscribed: Any,
val showOnMapScreen: Any,
val isLiked: Any,
val bypassChecks: Any,
val isHidden: Any,
val nsfw: Any,
val currentDirectEvents: Any,
val totalDirectEventsAfterLastJoin: Any,
val subscriptions: Any,
val userUuid: Any,
val username: Any,
val karma: Any,
val tsCreated: Any,
val allowsPrivateChats: Any,
val allowsNsfw: Any,
val thisUserBlockedCurrentUser: Any,
val incomingFriendshipRequest: Any,
val outstandingFriendshipRequest: Any,
val friends: Any,
val bio: Any,
val appRoleMapped: Any)

How to add whitespace in place of null values when reading from a file using SuperCSV?

I am trying to add column to the end of a file without changing the contents using SuperCSV and kotlin.
I cannot use CSVWriter due to limitation of resources.
So, my idea is to read from the original file row by row and add that to a string and have the result be used as a byte array.
fun addColumnToCSV(csvData: ByteArray, columnName: String, columnValue: String): ByteArray {
val prefs = CsvPreference.Builder(CsvPreference.STANDARD_PREFERENCE)
.useQuoteMode(NormalQuoteMode()).build()
val mapReader = CsvMapReader(BufferedReader(InputStreamReader(csvData.inputStream())), prefs)
val readHeader = mapReader.getHeader(true)
var row = mapReader.read(*readHeader)
var csv: String = readHeader.joinToString(",", postfix = ",$columnName\n")
while (row != null) {
val rowValue=readHeader.map { header-> row.getOrDefault(header,"\\s") }
csv += rowValue.joinToString(",", postfix = ",$columnValue\n")
row = mapReader.read(*readHeader)
}
csv = csv.trim()
mapReader.close()
return csv.toByteArray()
}
So, I have an example here and written a test for it.
#Test
fun `should add extra column in csv data when there are missing values`() {
val columnName = "ExtraColumn"
val columnValue = "Value"
val expectedCSV = "Name,LastName,$columnName\n" +
"John,Doe,$columnValue\n" +
"Jane,,$columnValue"
val csvData = "Name,LastName\n" + "John,Doe\n" + "Jane,"
val csv = addColumnToCSV(csvData.toByteArray(), columnName, columnValue)
Assertions.assertThat(String(csv)).isEqualTo(expectedCSV)
}
This test fails because the actual of csv data is
Name,LastName,ExtraColumn
John,Doe,Value
Jane,null,Value
I want it to be this, so that I am not changing the existing values that are present in the csv file.
Name,LastName,ExtraColumn
John,Doe,Value
Jane,,Value
I have tried with row.getOrDefault(header,"") its still the same result. How do I achieve this?
The problem seems to be on this line:
val rowValue=readHeader.map { header-> row.getOrDefault(header,"\\s") }
Without testing this, I would say that there's a null in row at index LastName, hence default value in getOrDefault is not applied because map contains the key.
Please try something like this:
val rowValue=readHeader.map { header-> row.getOrDefault(header,"\\s") ?: "" }

Is there a way to serialize a map in Kotlinx-Serialization

I'm using Kotlin 1.3.10 (and bound to this version) and Kotlinx-Serialization 0.13 and I'm having trouble with serializing a map in Kotlinx-Serialization.
I have the following code:
#Serializer(forClass = LocalDate::class)
object LocalDateSerializer : KSerializer<LocalDate> {
private val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
override val descriptor: SerialDescriptor
get() = StringDescriptor.withName("LocalDate")
override fun serialize(encoder: Encoder, obj: LocalDate) {
encoder.encodeString(obj.format(formatter))
}
override fun deserialize(decoder: Decoder): LocalDate {
return LocalDate.parse(decoder.decodeString(), formatter)
}
}
#Serializable
data class MyClass (
val students: Map<String,LocalDate>
)
#UnstableDefault
#Test
fun decodeEncodeSerialization() {
val jsonParser = Json(
JsonConfiguration(
allowStructuredMapKeys = true
)
)
val mc = MyClass(
mapOf("Alex" to LocalDate.of(1997,2,23))
)
val mcJson = jsonParser.stringify(MyClass.serializer(), mc)
val mcObject = jsonParser.parse(MyClass.serializer(), mcJson)
assert(true)
}
There is a red line when inspecting the code which says "Serializer has not been found for 'LocalDate'. To use context serializer as fallback, explicitly annotate type or property with #ContextualSerialization."
With other types of fields, it would have been enough to add #Serialization to it.
#Serializable
data class Student (
val name: String,
#Serializable(with = LocalDateSerializer::class)
val dob: LocalDate
)
But with a map I can't seem to figure out how. I put it above, or beside the object...
#Serializable
data class MyClass (
val students: Map<String,#Serializable(with = LocalDateSerializer::class) LocalDate> //here
//or
//#Serializable(with = LocalDateSerializer::class)
//val students2: Map<String, LocalDate> //here
)
...but tests still fail with
kotlinx.serialization.SerializationException: Can't locate argument-less serializer for class java.time.LocalDate (Kotlin reflection is not available). For generic classes, such as lists, please provide serializer explicitly.
And the workaround I have for it is
#Serializable
data class MyClass (
val students: List<Student>
)
#Serializable
data class Student (
val name: String,
#Serializable(with = LocalDateSerializer::class)
val dob: LocalDate
)
Is there a way I would not resort to the workaround? Thank you!
#file:UseSerializers(LocalDateSerializer::class)
put this in the file where your object is declared it should use LocalDateSerializer every time it sees Local Date

Issue while using kotlin reflaction to map object member properties to hashmap

open class Test {
fun getAsHashMap() : HashMap<String, Any> {
val hashMap = HashMap<String, Any>()
val className = this.javaClass.kotlin
for (prop in className::class.memberProperties) {
val field = className::class.java.getDeclaredField(prop.name)
val fieldSerializedName : SerializedName? = field.getAnnotation(SerializedName::class.java)
fieldSerializedName?.let {
hashMap[fieldSerializedName.value] = prop.get(this)!!
} ?: run {
hashMap[prop.name] = prop.get(this)!!
}
}
return hashMap
}
}
I have wrote above function to map the memberProperties of object instance of its child class to hashmap. It either uses serialized name of the member or prop name [Based on availability of serialized name for that property]
But unfortunately I get the following error.
This is my first time using reflection java/kotlin, please let me know if it can be fixed.
Edit 1:
It works perfectly if I use name of the this.javaClass.kotlin directly like this
data class ProductInformation (
#field:SerializedName("productid")
val productId: Int,
#field:SerializedName("productname")
val productName: String,
#field:SerializedName("brandname")
val brandName: String,
#field:SerializedName("originalprice")
val originalPrice: Int,
#field:SerializedName("sellingprice")
val sellingPrice: Int,
#field:SerializedName("productgender")
val productGender: String,
#field:SerializedName("productvariant")
val productVariant: String,
#field:SerializedName("discounted")
val discounted: String,
#field:SerializedName("productcategory")
val productCategory: String
) : StructuredEventAttribute {
override fun getAsHashMap(): HashMap<String, Any> {
val hashMap = HashMap<String, Any>()
for (prop in ProductInformation::class.memberProperties) {
val field = ProductInformation::class.java.getDeclaredField(prop.name)
val fieldSerializedName : SerializedName? = field.getAnnotation(SerializedName::class.java)
fieldSerializedName?.let {
hashMap[fieldSerializedName.value] = prop.get(this)!!
} ?: run {
hashMap[prop.name] = prop.get(this)!!
}
}
return hashMap
}
}
interface StructuredEventAttribute {
fun getAsHashMap() : HashMap<String, Any>
}
It works perfectly fine
ProductInformation::class.memberProperties returns a collection of ProductInformation class member properties.
className::class.memberProperties (where className = this.javaClass.kotlin) returns a collection of member properties of class of className, which is KClass<out Test>. In short you are getting members of KClass instead of Test.
Solution: change className::class.memberProperties to className.memberProperties.

How to setup csv component to map list of strings

I have a csv file that holds country names and years they won on the eurovision:
country, year
Israel, 1998
Sweden, 2012
Sweden, 2015
United Kingdom, 1997
and my csv (using tototoshi):
object CountryEurovision {
def countrEurovisionYearFile: File = new File("conf/countryEurovision.csv")
lazy val countrEurovisionYearMap: Map[String, String] = getConvertData
private def getConvertData: Map[String, String] = {
implicit object CodesFormat extends CSVFormat {
val delimiter: Char = ','
val quoteChar: Char = '"'
val escapeChar: Char = '"'
val lineTerminator: String = "\r\n"
val quoting: Quoting = QUOTE_NONNUMERIC
val treatEmptyLineAsNil: Boolean = false
}
val csvDataReader = CSVReader.open(countrEurovisionYearFile, "UTF-8")(CodesFormat)
val linesIterator = csvDataReader.iteratorWithHeaders
val convertedData = linesIterator.map {
row => row("Country") -> row("Year")
}.toMap
csvDataReader.close()
convertedData
}
}
now, since the country and year is not unique, cause a country can have several years when they won, so when I get Sweden:
CountryEurovision.countrEurovisionYearMap.get("Sweden")
I only get option res0: Option[String] = Some(2015)
which I would expect to be the list of years per country... even if it's a country of just one year I will get a list, and in case of Sweden I will get list of 2012 and 2015...
How can I change my setup for that behavior?
When you transform linesIterator.map { row => row("Country") -> row("Year") } into a Map with .toMap, for duplicated keys only the last one will be kept as it will override the previous one.
You can change this by having a unique element per key (country) by grouping values (dates) per key (before applying toMap) and modifying the value of your Map to be a List:
linesIterator
.map { row => row("Country") -> row("Year") } // List(("Sweden", 1997), ("France", 2008), ("Sweden", 2017))
.groupBy(_._1) // Map(France -> List((France,2008)), Sweden -> List((Sweden,1997), (Sweden,2017)))
.mapValues(_.map(_._2)) // Map(France -> List(2008), Sweden -> List(1997, 2017))
.toMap
which produces:
Map(France -> List(2008), Sweden -> List(1997, 2017))
This way, .get("Sweden") will return Some(List(1997, 2017)).

Categories