Spring boot annotation #ConfigurationProperties doesn't work correctly classes with nested collection in Kotlin - spring

There is the data class with #ConfigurationProperties and #ConstructorBinding. This class contains the field which is collection.
There are several property sources ( application.yml, application-dev1.yml) which initialize the first element of the collection.
Binding for this element doesn't work correctly. Values for initialazation pulls only from one property source.
Expected beahavior is the same as for field of type of some nested class: merging values from all property sources.
Kotlin properties class
#ConfigurationProperties("tpp.test.root")
#ConstructorBinding
data class RootPropperties(
var rootField1: String = "",
var rootField2: String = "",
var nested: NestedProperties = NestedProperties(),
var nestedList: List<NestedListProperties> = listOf()
) {
data class NestedProperties(
var nestedField1: String = "",
var nestedField2: String = ""
)
#ConstructorBinding
data class NestedListProperties(
var nestedListField1: String = "",
var nestedListField2: String = ""
)
}
application.yml
tpp:
test:
root:
root-field1: default
nested:
nested-field1: default
nested-list:
- nested-list-field1: default
application-dev1.yml
tpp:
test:
root:
root-field2: dev1
nested:
nested-field2: dev1
nested-list:
- nested-list-field2: dev1
Test
#ActiveProfiles("dev1")
#SpringBootTest
internal class ConfigurationPropertiesTest {
#Autowired
lateinit var environment: Environment
#Autowired
lateinit var rootPropperties: RootPropperties
#Test
fun `configuration properties binding`() {
Assertions.assertEquals("default", rootPropperties.rootField1)
Assertions.assertEquals("dev1", rootPropperties.rootField2)
Assertions.assertEquals("default", rootPropperties.nested.nestedField1)
Assertions.assertEquals("dev1", rootPropperties.nested.nestedField2)
Assertions.assertTrue(rootPropperties.nestedList.isNotEmpty())
//org.opentest4j.AssertionFailedError:
//Expected :default
//Actual :
Assertions.assertEquals("default", rootPropperties.nestedList[0].nestedListField1)
Assertions.assertEquals("dev1", rootPropperties.nestedList[0].nestedListField2)
}
#Test
fun `environment binding`() {
Assertions.assertEquals("default", environment.getProperty("tpp.test.root.root-field1"))
Assertions.assertEquals("dev1", environment.getProperty("tpp.test.root.root-field2"))
Assertions.assertEquals("default", environment.getProperty("tpp.test.root.nested.nested-field1"))
Assertions.assertEquals("dev1", environment.getProperty("tpp.test.root.nested.nested-field2"))
Assertions.assertEquals("default", environment.getProperty("tpp.test.root.nested-list[0].nested-list-field1"))
Assertions.assertEquals("dev1", environment.getProperty("tpp.test.root.nested-list[0].nested-list-field2"))
}
}
The test with RootProperties failed on assertEquals("default", rootPropperties.nestedList[0].nestedListField1) because rootPropperties.nestedList[0].nestedListField1 has empty value. All other assertions tests pass sucessfully. The binding doesn't work correctly just for collection.
At the same time the test with Environment passed successfully. And Environment.getProperty("tpp.test.root.nested-list[0].nested-list-field1") resolves corrected value: "default".
Spring boot version: 2.6.4

covered in this section of the reference documention
Possible workaround could be to switch List to a Map.
Properties class
#ConfigurationProperties("tpp.test.root-map")
#ConstructorBinding
data class RootMapPropperties(
var rootField1: String = "",
var rootField2: String = "",
var nested: NestedProperties = NestedProperties(),
var nestedMap: Map<String, NestedMapProperties> = mapOf()
) {
data class NestedProperties(
var nestedField1: String = "",
var nestedField2: String = ""
)
data class NestedMapProperties(
var nestedMapField1: String = "",
var nestedMapField2: String = ""
)
}
application.yml
tpp:
test:
root-map:
root-field1: default
nested:
nested-field1: default
nested-map:
1:
nested-map-field1: default
application-dev1.yml
tpp:
root-map:
root-field2: dev1
nested:
nested-field2: dev1
nested-map:
1:
nested-map-field2: dev1
Test
#ActiveProfiles("dev1")
#SpringBootTest
internal class ConfigurationPropertiesMapTest {
#Autowired
lateinit var environment: Environment
#Autowired
lateinit var rootPropperties: RootMapPropperties
#Test
fun `configuration properties binding`() {
Assertions.assertEquals("default", rootPropperties.rootField1)
Assertions.assertEquals("dev1", rootPropperties.rootField2)
Assertions.assertEquals("default", rootPropperties.nested.nestedField1)
Assertions.assertEquals("dev1", rootPropperties.nested.nestedField2)
Assertions.assertTrue(rootPropperties.nestedMap.isNotEmpty())
Assertions.assertEquals("default", rootPropperties.nestedMap["1"]!!.nestedMapField1)
Assertions.assertEquals("dev1", rootPropperties.nestedMap["1"]!!.nestedMapField2)
}
}

Related

Kotlin sealed class Jackson different place in memory

Recently, I have started with Kotlin and I encountered some strange behavior while testing JSON mapping with Spring.
I created something like this:
#SpringBootTest(classes = [TestApplication::class])
class JacksonIntegrationTest {
#Autowired
lateinit var objectMapper: ObjectMapper
var objectMapperTest = TestObjectMapper()
#Test
fun `should serialize and deserialize object`() {
//given
val value = SealedObject
//when
val jsonTest = objectMapperTest.writeValueAsString(value)
val resultTest: SealedObject = objectMapperTest.readValue(jsonTest)
val json = objectMapper.writeValueAsString(value)
val result: SealedObject = objectMapper.readValue(json)
//then`
assertThat(result).isSameAs(value)
assertThat(resultTest).isSameAs(value) <---------- FAILED
}
internal sealed class Sealed
internal object SealedObject: Sealed()
}
value = JacksonIntegrationTest$SealedObject#6727e0cd <-------------\
result (SPRING) = JacksonIntegrationTest$SealedObject#6727e0cd <----- SAME MEMORY PLACE
resultTest (OWN) = JacksonIntegrationTest$SealedObject#3c8e3f98
As you can see, spring objectmapper returned value with same reference at memory as origin value.
But own created ObjectMapper returned object at different place at memory. Why?
All results should've same memory place
Okay, I've found a solution.
.registerModule(
kotlinModule(
initializer = {
configure(KotlinFeature.SingletonSupport, true)
},
),
)

Spring boot serialize kotlin enum by custom property

I have an Enum and I would like to serialize it using custom property. It works in my tests but not when I make request.
Enum should be mapped using JsonValue
enum class PlantProtectionSortColumn(
#get:JsonValue val propertyName: String,
) {
NAME("name"),
REGISTRATION_NUMBER("registrationNumber");
}
In test the lowercase case works as expected.
class PlantProtectionSortColumnTest : ServiceSpec() {
#Autowired
lateinit var mapper: ObjectMapper
data class PlantProtectionSortColumnWrapper(
val sort: PlantProtectionSortColumn,
)
init {
// this works
test("Deserialize PlantProtectionSortColumn enum with custom name ") {
val json = """
{
"sort": "registrationNumber"
}
"""
val result = mapper.readValue(json, PlantProtectionSortColumnWrapper::class.java)
result.sort shouldBe PlantProtectionSortColumn.REGISTRATION_NUMBER
}
// this one fails
test("Deserialize PlantProtectionSortColumn enum with enum name ") {
val json = """
{
"sort": "REGISTRATION_NUMBER"
}
"""
val result = mapper.readValue(json, PlantProtectionSortColumnWrapper::class.java)
result.sort shouldBe PlantProtectionSortColumn.REGISTRATION_NUMBER
}
}
}
But in controller, when i send request with lowercase I get 400. But when the request matches the enum name It works, but response is returned with lowercase. So Spring is not using the objectMapper only for request, in response it is used.
private const val RESOURCE_PATH = "$API_PATH/plant-protection"
#RestController
#RequestMapping(RESOURCE_PATH, produces = [MediaType.APPLICATION_JSON_VALUE])
class PlantProtectionController() {
#GetMapping("/test")
fun get(
#RequestParam sortColumn: PlantProtectionSortColumn,
) = sortColumn
}
I believe kqr's answer is correct and you need to configure converter, not JSON deserializer.
It could look like:
#Component
class StringToPlantProtectionSortColumnConverter : Converter<String, PlantProtectionSortColumn> {
override fun convert(source: String): PlantProtectionSortColumn {
return PlantProtectionSortColumn.values().firstOrNull { it.propertyName == source }
?: throw NotFoundException(PlantProtectionSortColumn::class, source)
}}
In your endpoint you are not parsing json body but query parameters, which are not in json format.

How to wire #Configuration files from Unit Test - Spring Boot + Kotlin

I have an application.yml with some configuration properties required by my application.
SF:
baseurl: https://xxxxx
case:
recordTypeId: 0124a0000004Ifb
application:
recordTypeId: 0125P000000MkDa
address:
personal:
recordTypeId: 0125P000000MnuO
business:
recordTypeId: 0125P000000MnuT
I have defined a configuration class to read those properties as follows:
#Configuration
class SFProperties(
#Value("\${sf.case.recordTypeId}") val caseRecordTypeId: String,
#Value("\${sf.application.recordTypeId}") val applicationRecordTypeId: String,
#Value("\${sf.address.personal.recordTypeId}") val addressPersonalRecordTypeId:String,
#Value("\${sf.address.business.recordTypeId}") val addressBusinessRecordTypeId: String
)
The class is wired within a service without any issues,
#Service
class SFClientManagementServiceImpl( val webClientBuilder: WebClient.Builder):
ClientManagementService {
....
#Autowired
lateinit var sfProperties: SFProperties
override fun createCase(caseRequest: CaseRequestDto): Mono<CaseResponseDto> {
...
var myValue= sfProperties.caseRecordTypeId
....
}
}
When trying to test this service, I get a "lateinit property sfProperties has not been initialized" exception:
The test looks as follows:
#SpringBootTest(classes = [SFProperties::class])
class SalesforceClientManagementServiceImplTests {
#Autowired
open lateinit var sfProperties: SFProperties
#Test
fun `createCase should return case id when case is created`() {
val clientResponse: ClientResponse = ClientResponse
.create(HttpStatus.OK)
.header("Content-Type", "application/json")
.body(ObjectMapper().writeValueAsString(Fakes().GetFakeCaseResponseDto())).build()
val shortCircuitingExchangeFunction = ExchangeFunction {
Mono.just(clientResponse)
}
val webClientBuilder: WebClient.Builder = WebClient.builder().exchangeFunction(shortCircuitingExchangeFunction)
val sfClientManagementServiceImpl =
SFClientManagementServiceImpl(webClientBuilder)
var caseResponseDto =
salesforceClientManagementServiceImpl.createCase(Fakes().GetFakeCaseRequestDto())
var response = caseResponseDto.block()
if (response != null) {
assertEquals(Fakes().GetFakeCaseResponseDto().id, response.id)
}
}
I have tried many other annotations on the Test class but without success, I would appreciate any ideas.

Is there anyway to update #Bean at runtime?

For my project I want to download from an API and store this information in a map. Furthermore I want to have the map as a bean in another class. I suspect the API to update regularly so I have set a #Schedule for downloading the XML file from the API.
To the problem... How can I update the map with the information from the API every time the XML is downloaded. I do not want to reboot the application each time.
I am very new to the Spring framework so if there is a more elegant method to do this please let me know.
data class DataContainer(val dictionary: MutableMap<String, String>)
#Configuration
#Component
class DownloadRenhold {
var dict: MutableMap<String, String> = xmlToDict("/renhold.xml")
val dataContainer: DataContainer
#Bean
get() = DataContainer(dict)
fun download(link: String, path: String) {
URL(link).openStream().use { input ->
FileOutputStream(File(path)).use { output ->
input.copyTo(output)
}
}
}
#Scheduled(fixedRate = 5000)
fun scheduledDL() {
download("www.link.com","src/main/resources/renhold.xml")
dict = xmlToDict("/renhold.xml")
}
class Controller {
#GetMapping(value = ["/{orgnummer}"]) // #RequestMapping(value="/",method=RequestMethod.GET)
fun orgNrRequest(#PathVariable("orgnummer") nr: String): String? {
var actx = AnnotationConfigApplicationContext(DownloadRenhold::class.java)
var dataContainer = actx.getBean(DataContainer::class.java)
return dataContainer.dictionary[nr]
}
```
I would suggest to not have DataContainer as a bean directly. Instead inject DownRenhold into Controller as a singleton bean. Something along these lines:
// No need to make this class a Configuration. Plain Component would suffice.
// #Configuration
#Component
class DownloadRenhold {
var _dataContainer: DataContainer = null
var dataContainer: DataContainer
get() = _dataContainer
#Scheduled(fixedRate = 5000)
fun scheduledDL() {
_dataContainer = // do your download thing and create a DataContainer instance.
}
}
class Controller {
#Autowired
var dataProvider: DownloadRenhold
#GetMapping(value = ["/{orgnummer}"])
#RequestMapping(value="/",method=RequestMethod.GET)
fun orgNrRequest(#PathVariable("orgnummer") nr: String): String? {
dataProvider.dataContainer // access the current data container
}

what is difference implement interface at cglib proxy?

I made 2 proxy object using ProxyFactory in Spring.
One proxy object used interface and one proxy object not used interface.
but not working jdk dynamic proxy. all proxy object used cglib.
The proxy object that implement interface call real method.
The proxy object that not implement interface has unexpected result.
What's the difference between two cglib proxy object?
The only difference between the two is the interface.
// Not implement interface
open class Person: AbstractPerson() {
}
abstract class AbstractPerson(var age: Int? = null,
var name: String? = null) {
fun init() {
this.age = 31
this.name = "LichKing"
}
fun introduce(): String = "age: $age name: $name"
}
// Implement interface
open class PersonImpl: AbstractPersonImpl() {
}
abstract class AbstractPersonImpl(var age: Int? = null,
var name: String? = null): PersonInterface {
fun init() {
this.age = 31
this.name = "LichKing"
}
override fun introduce(): String = "age: $age name: $name"
}
interface PersonInterface {
fun introduce(): String
}
// Test
class PersonTest {
#Test
fun implementInterface() {
val p = PersonImpl()
p.init()
val proxyFactory: ProxyFactory = ProxyFactory()
proxyFactory.setTarget(p)
val proxy = proxyFactory.proxy as PersonImpl
println(proxy.javaClass)
println(proxy.introduce()) // "age: 31 name: LichKing"
}
#Test
fun notImplementInterface() {
val p = Person()
p.init()
val proxyFactory: ProxyFactory = ProxyFactory()
proxyFactory.setTarget(p)
val proxy = proxyFactory.proxy as Person
println(proxy.javaClass)
println(proxy.introduce()) // "age: null name: null"
}
}
kotlin method's default option is final.
The cause is introduce method not be extend.
default option is open when using interface so it's could be extended.
gradle plugin kotlin-spring is only for spring annotations.
It does not work for abstract class.

Resources