MapStruct in Kotlin: How to map from multiple sources into one model? - spring-boot

Let's say I have model:
data class MyModel(
var firstName: String,
var lastName: String,
)
And let's say I'm attempting to map some properties into this model from my source PersonInfo
public final data class PersonInfo(
val firstName: String,
val lastName: String,
)
This is easy enough; I just run
fun persontoToModel(personInfo: PersonInfo): MyModel
Now let's say MyModal is now:
data class MyModel(
var firstName: String,
var lastName: String,
var dateOfBirth: String,
var licenseNumber: String,
)
Let's also say not all of this information is available in PersonInfo. Instead, dateOfBirth is available in PersonDetails, and licenseNumber is available in PersonDocuments, both of which also include a bunch of other properties that I don't necessarily need to map in this case.
PersonDetails and PersonDocuments:
public final data class PersonDetails(
val dateOfBirth: LocalDate,
val weight: String,
val height: String,
val eyeColor: String,
)
public final data class PersonDocuments(
val licenseNumber: String,
val passportNumber: String,
val socialSecurityNumber: String,
)
From my understanding from the documentation, I can manually map these fields by specifying them with the source in a Mapping annotation as follows:
#Mapping(source="personDetails.dateOfBirth", target="dateOfBirth")
#Mapping(source="personDocuments.licenseNumber", target="licenseNumber")
#BeanMapping(ignoreUnmappedSourceProperties = [ all the other stuff I don't need])
fun persontoToModel(personInfo: PersonInfo, personDetails: PersonDetails, personDocuments: PersonDocuments): MyModel
Except this doesn't work. First and foremost, the second mapping annotation is highlighted as error with the reason "This annotation is not repeatable." I'm not sure if it's a Kotlin thing, since the documentations on MapStruct seem to only use Java examples.

#Mappings(
Mapping(source="personDetails.dateOfBirth", target="dateOfBirth"),
Mapping(source="personDocuments.licenseNumber", target="licenseNumber")
)
#BeanMapping(ignoreUnmappedSourceProperties = [ all the other stuff I don't need])
fun persontoToModel(personInfo: PersonInfo, personDetails: PersonDetails, personDocuments: PersonDocuments): MyModel
As of Kotlin 1.1, repeated annotations are not supported (see
KT-12794). You have to wrap the Mapping-Annotation in a
Mappings-Annotation.
Source

Related

Polymorphic #RequestBody in Spring-Boot

The problem's pretty straightforward. I have a couple of events that derive from the same interface, and I'd like to deserialize them to their propper super-class.
I know how to do that with an object mapper, but using my own mapper would mean letting Spring-Boot parse the #RequestBody as a String and then doing it myself, which isn't the worlds end, but I can't help but suspect that Spring provides proper tools to handle this kind of situation. Trouble is, I can't seem to find them.
Here's a bit of sample code:
example event:
interface YellowOpsEvent {
val user: String
val partner: String
val subject: String
val change: NatureOfChange
}
data class StatusChangedEvent(override val user: String,
override val partner: String,
override val subject: String,
val before: String,
val after: String): YellowOpsEvent {
override val change = NatureOfChange.Changed
}
controller:
#PostMapping("/event")
fun writeEvent(#RequestBody event: YellowOpsEvent) { // < I expect this not to throw an exception
val bugme = event is StatusChangedEvent // < I expect this to return true if I send the proper event data.
}
Just to clarify, I perfectly understand why this doesn't work out of the box. The trouble is, I can't find out what I need to do to make it work.
The link in pL4Gu33's comment lead me in the right direction, but it took some additional searching and fiddling, plucking information from here and there to arrive at the solution that would finally work, so I'm summarising it here for completeness.
The trouble is that you'll need two annotations, one on the interface and one on the implementing classes, the combined use of which seems somewhat ill-documented.
First, on the interface, add this annotation. Contrary to some tutorials you will find, no further annotation of the interface is required:
#JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.PROPERTY, property="#class")
interface YellowOpsEvent {
val user: String
val partner: String
val subject: String
val change: NatureOfChange
}
According to some documentation, this alone should be enough for propper deserialisation. The spring-boot controller, however, will throw an exception because the passed root name does not match the class it was expecting.
// the above will throw an exception when the serialization product is sent to this controller:
#PostMapping("/event")
fun writeEvent(#RequestBody event: YellowOpsEvent) { // < I expect this not to throw an exception
val bugme = event is StatusChangedEvent // < I expect this to return true if I send the proper event data.
}
To fix that, add the #JsonRootName annotation to any implementing classes, with the interface's name. Most documentation of this annotation don't use it for this, instead just for renaming the type, and even when it's mentioned in the linked question in the context of polymorphism, it wrongly uses its own name. This is what it needs to look like:
#JsonRootName("YellowOpsEvent")
data class StatusChangedEvent(override val user: String,
override val partner: String,
override val subject: String,
val before: String,
val after: String): YellowOpsEvent {
override val change = NatureOfChange.Changed
}
Now it works! :)

Kotlin JPA query inner join using two types

I am new to Kotlin and JPA. I have an inner join query that gets data from two tables(Postgres). The query works fine. However, since I now have two types (the two tables), using either one only returns all the fields from one of the tables. In order to return all fields, I changed the type to List. However, when I do that, my oject that is returned has no fields, only the raw data. How can I change my code so my json response contains both the name of the fields, as well as the data.
Sorry if my question isn't clear, I'm very new to Kotlin.
UPDATED CODE
my repository code
package com.sg.xxx.XXXTTT.report.repository
import com.sg.xxx.XXXTTT.report.model.Report
import com.sg.xxx.XXXTTT.report.model.ReportWithBatches
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import java.time.LocalDate
#Repository
interface IReportRepository : JpaRepository<Report, Long> {
fun findAllByCreationDate(date: LocalDate): List<Report>
fun findByReportName(name: String): Report?
fun findByAdlsFullPath(name: String): Report?
#Query("SELECT new com.sg.xxx.xxxttt.report.model.ReportWithBatches(r.adlsFullPath, r.sentToXXX, r.contentLength, r.creationDate, r.remoteFileNameOnFTA, b.dataPath , b.version, b.source, r.numberOfRecords) FROM Report r INNER JOIN BatchInfo b ON r.reportUuid = b.reportUuid WHERE r.creationDate = ?1")
fun findAllByCreationDateJoinBatches(date: LocalDate): List<ReportWithBatches>
}
my controller code
#GetMapping(value = ["/linkBatches/{today}"])
fun findAllByCreationDateJoinBatches(#PathVariable("today") #DateTimeFormat(pattern = "yyyyMMdd") date: LocalDate): List<ReportWithBatches> {
return eligibleService.findAllByCreationDateJoinBatches(date)
}
my DTO
package com.sg.xxx.xxxttt.report.model
import java.time.LocalDate
open class ReportWithBatches(
var adlsFullPath: String?,
var sentToXXX: Boolean?,
var contentLength: Long?,
var creationDate: LocalDate,
var remoteFileNameOnFTA: String?,
var dataPath: String?,
var version: Int?,
var source: String?,
var numberOfRecords: Long?
)
my function in the service
fun findAllByCreationDateJoinBatches(date: LocalDate): List<ReportWithBatches> {
return reportRepository.findAllByCreationDateJoinBatches(date)
}
}
As was correctly stated in the comments, the return type of your query is List<Array<Any?>>, not List<Any>.
Create a data class that would serve as your DTO and map results to it:
data class ReportWithBatchInfo(val azureFileName : String, /* more field here */)
fun findAllByCreationDateJoinBatches(date: LocalDate): List<ReportWithBatchInfo> {
return reportRepository.findAllByCreationDateJoinBatches(date).map {
ReportWithBatchInfo(it[0] as String, /* more mappings here */)
}
}

Swagger 2 UI How to show models that are not explicitly returned by RestController

I'm having following issue, on swagger under Models, i see just abstract Base class that is extended by 3 other classes. My current end point returns Base type of class, because i can have 3 different types returned on one end point.
So basically i have something like this
#MappedSuperclass
#ApiModel(description = "Base Details.")
abstract class BaseClass(
open var id: String? = null,
var prop1: String? = null,
var prop2: String? = null,
var prop3: String? = null,
var prop4: String? = null
)
#ApiModel(description = "Some Specific Details that contains all base properties.")
data class AnotherClass(
val prop4: String,
val prop5: String,
val prop6: Set<Amount>,
val prop7: Set<Amount>,
val prop8: String
) : BaseClass()
#ApiModel(description = "Some more Specific Details that contains all base properties.")
data class OneMoreClass(
val prop4: String,
val prop5: String
) : BaseClass()
And in RestController i have this
#GetMapping
#ApiOperation(value = "End point description", notes = "Notes notes notes.")
fun getSomethingFromDatabase(): List<BaseClass> {
return someService.getData();
}
So issue that i have is on swagger UI, under Models section i see just BaseClass and no other classes at all...
I tried this, because somewhere i seen this example:
#ApiModel(description = "Base Details.", subTypes = {AnotherClass.class})
BaseClass
but this way i have "kotlin" issue, that is saying "name is missing", also i can not do AnotherClass::class...
You will have to add those in the config as below:
return new Docket(DocumentationType.SWAGGER_2)
.additionalModels(typeResolver.resolve(AnotherClass.class), typeResolver.resolve(OneMoreClass.class))
.....
subTypes is still not completely supported in Swagger 2, still has an open ticket
For your Kotlin config, this is how it should look like:
subTypes = [AnotherClass::class, OneMoreClass::class]
I have just added a sample Kotlin controller for you to refer in my github project. Look for AnimalController.kt & SwaggerConfig for required setup.

Enums in kotlin

I want to access the keys for an item in enum class
enum class Events {
REFER_AND_EARN {
val key: String = "Refer and Earn"
val source: String = "Source"
},
REFILL_PAST_MEDICINE_CLICK {
val key: String = "Refill Past Medicine Click"
val source: String = "Source"
val pointOfInitiation: String = "Point of initiation"
}
}
Like for the above enum class can I access source like this??
Events.REFER_AND_EARN.source
You can do what you want to achieve by writing this:
enum class Events(val key: String, val source: String, val pointOfInitiation: String? = null) {
REFER_AND_EARN(key = "Refer and Earn", source = "Source"),
REFILL_PAST_MEDICINE_CLICK(
key = "Refill Past Medicine Click",
source = "Source",
pointOfInitiation = "Point of initiation"
)
}
Enum constants do not declare new types themselves. This means that you can't simply access these properties: they are public, but there's no access to the type where they are declared.
You can implement an interface by enum and expose these properties by overriding ones from interface.
Or you can declare a sealed class instead of enum class and use object declarations instead of enum constants.
You need to use properties instead:
enum class Events(val key: String,
val source: String,
val pointOfInitiation: String) {
REFER_AND_EARN("Refer and Earn",
"Source",
"Unknown"),
REFILL_PAST_MEDICINE_CLICK(
"Refill Past Medicine Click",
"Source",
"Point of initiation"
);
}
Or you can use a sealed class as others mentioned.

Kotlin not nullable value can be null?

I have backend that return me some json.
I parse it to my class:
class SomeData(
#SerializedName("user_name") val name: String,
#SerializedName("user_city") val city: String,
var notNullableValue: String
)
Use gson converter factory:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(ENDPOINT)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
And in my interface:
interface MyAPI {
#GET("get_data")
Observable<List<SomeData>> getSomeData();
}
Then I retrieve data from the server (with rxJava) without any error. But I expected an error because I thought I should do something like this (to prevent GSON converter error, because notNullableValue is not present in my JSON response):
class SomeData #JvmOverloads constructor(
#SerializedName("user_name") val name: String,
#SerializedName("user_city") val city: String,
var notNullableValue: String = ""
)
After the data is received from backend and parsed to my SomeData class with constructor without def value, the value of the notNullableValue == null.
As I understand not nullable value can be null in Kotlin?
Yes, that is because you're giving it a default value. Ofcourse it will never be null. That's the whole point of a default value.
Remove ="" from constructor and you will get an error.
Edit: Found the issue. GSON uses the magic sun.misc.Unsafe class which has an allocateInstance method which is obviously considered very unsafe because what it does is skip initialization (constructors/field initializers and the like) and security checks. So there is your answer why a Kotlin non-nullable field can be null. Offending code is in com/google/gson/internal/ConstructorConstructor.java:223
Some interesting details about the Unsafe class: http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
Try to override constructor like this:
class SomeData(
#SerializedName("user_name") val name: String,
#SerializedName("user_city") val city: String,
var notNullableValue: String = "") {
constructor() : this("","","")
}
Now after server response you can check the notNullableValue is not null - its empty

Resources