In Java I would do validation when creating constructor in domain object, but when using data class from kotlin I don't know how to make similar validation. I could do that in application service, but I want to stick to domain object and it's logic. It's better to show on example.
public class Example {
private String name;
Example(String name) {
validateName(name);
this.name = name;
}
}
In Kotlin I have just a data class is there a way to do it similarly to Java style?
data class Example(val name: String)
You can put your validation code inside an initializer block. This will execute regardless of whether the object was instantiated via the primary constructor or via the copy method.
data class Example(val name: String) {
init {
require(name.isNotBlank()) { "Name is blank" }
}
}
A simple example:
fun main() {
println(Example(name = "Alice"))
println(try { Example(name = "") } catch (e: Exception) { e })
println(try { Example(name = "Bob").copy(name = "") } catch (e: Exception) { e })
}
Produces:
Example(name=Alice)
java.lang.IllegalArgumentException: Name is blank
java.lang.IllegalArgumentException: Name is blank
You can get a similar effect by using companion factory method
:
data class Example private constructor(val name: String) {
companion object {
operator fun invoke(name: String): Example {
//validateName
return Example(name)
}
}
}
...
val e = Example("name")
e.name //validated
You may want to use the interface to hide the data class.
The amount of code will increase slightly, but I think it's more powerful.
interface Example {
val id: String
val name: String
companion object {
operator fun invoke(name: String): Example {
// Validate ...
return ExampleData(
id = UUID.randomUUID().toString(),
name = name
)
}
}
fun copy(name: String): Example
operator fun component1() : String
operator fun component2() : String
}
private data class ExampleData(override val id: String, override val name: String): Example {
override fun copy(name: String): Example = Example(name)
}
Related
I have this method
fun delete(id: Long) {
NotFoundExceptionValidator(!dishOfTheDayEntityRepository.existsById(id), "dishOfTheDay not found")
dishOfTheDayEntityRepository.deleteById(id)
}
NotFoundExceptionValidator this just checks if it's null then throws error
this is what I tried
#ConcurrentExecution
internal class DishOfTheDayServiceTest {
private val repo: DishOfTheDayEntityRepository = mockk()
private val mapper: DishOfTheDayMapper = mockk()
private val dishOfTheDayEntityService = DishOfTheDayService(repo, mapper)
#Test
fun `delete should work properly`() {
//given
val id: Long = 1;
//when
dishOfTheDayEntityService.delete(1)
//then
verify(exactly = 1) { repo.deleteById(1) }
}
}
when i run it it throws this error
no answer found for: DishOfTheDayEntityRepository(#1).existsById(1)
io.mockk.MockKException: no answer found for: DishOfTheDayEntityRepository(#1).existsById(1)
You forgot to mock your mocks behaviour, i.e. you should explicitly specify what the existsById() and deleteById() methods return. For example for existsById() it should look like:
every { repo.existsById(id) } returns true
I suppose that the deleteById() method returns Unit so if you don't want to do it like above you can mock DishOfTheDayEntityRepository like:
private val repo: DishOfTheDayEntityRepository = mockk(relaxUnitFun = true)
Now you don't have to mock Unit returning methods of DishOfTheDayEntityRepository. You can find more about it here.
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.
I have a strange problem. When I test the "edit" method I receive an exception. I know how to fix it: I have to add null checking of "it" in the "let" block in the "edit" method - but this situation shouldn't ever have a place. Let me know what you think about it. What should I change?
Controller:
#PatchMapping("{id}")
#ResponseStatus(HttpStatus.ACCEPTED)
fun edit(#PathVariable id: Long, #Valid #RequestBody baseData: EditOrganizationRequest?) =
manageOrganizationService.update(id, baseData!!).let {
EntityModel.of(it)
}
Service:
override fun update(id: Long, baseData: EditOrganizationRequest): OrganizationEntity {
return findOrganization(id).apply {
name = baseData.name
organizationRepository.save(this)
}
}
private fun findOrganization(id: Long) = organizationRepository.findByIdOrNull(id)
?: throw ResourceNotFoundException()
Test:
#Test
fun `#edit should return "not found" error`() {
val baseData = EditOrganizationRequest("New name")
given(manageOrganizationService.fetchOne(1))
.willThrow(ResourceNotFoundException::class.java)
mvc.patch("/organizations/1") {
contentType = APPLICATION_JSON
content = asJsonString(baseData)
}.andExpect {
status { isBadRequest() }
content { contentType(HAL_JSON_VALUE) }
}
}
Test result:
Request processing failed; nested exception is
java.lang.IllegalArgumentException: Content must not be null!
I'm an idiot. I mocked another method then I used.
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
}
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.