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.
Related
I am testing a REST controller, and I'd like to inject mock service.
But I am getting a null value when calling service Mock
this is my code:
Interface:
interface CaseManagementService {
fun createAccount(caseRequest: CaseRequestDto): Mono<CaseResponseDto>
}
Service:
#Service
class CaseManagementServiceImpl(private val clientManagementService:
ClientManagementService) : CaseManagementService {
override fun createAccount(caseRequest: CaseRequestDto): Mono<CaseResponseDto> {
return clientManagementService.createAccount(caseRequest)
}
}
Controller:
#RestController
#RequestMapping("somepath")
class CaseController(private val caseManagementService: CaseManagementService) {
#PostMapping()
fun createCase(#RequestBody caseRequest: CaseRequestDto): Mono<CaseResponseDto> {
return caseManagementService.createAccount(caseRequest) }
}
The test:
#SpringBootTest
class CaseControllerTests {
#Test
fun `createCase should return case id when a case is created`() {
val caseManagementService: CaseManagementServiceImpl =
Mockito.mock(CaseManagementServiceImpl::class.java)
val caseResponseDtoMono = Mono.just(Fakes().GetFakeCaseResponseDto())
val requestDto = Fakes().GetFakeCaseRequestDto()
`when`(caseManagementService.createAccount(requestDto).thenReturn(caseResponseDtoMono))
var caseController = CaseController(caseManagementService)
//NULL EXCEPTION HAPPENS HERE - RETURNS NULL THIS CALL
var result = caseController.createCase(Fakes().GetFakeCaseRequestDto())
StepVerifier.create(result)
.consumeNextWith { r -> assertEquals(Fakes().GetFakeCaseResponseDto().id, r.id)
}.verifyComplete()
}
}
The closing bracket is in a wrong place: you are calling Mono.thenReturn (on a null Mono instance returned from createAccount) instead of the Mockito's thenReturn (I assume that's what you meant):
`when`(caseManagementService.createAccount(requestDto)).thenReturn(caseResponseDtoMono)
Second problem: you are mocking createAccount call for a specific instance of the CaseRequestDto. In the actual call you are using different instance, so the arguments do not match and the mock returns null. Try reusing the request instance, i.e.:
var result = caseController.createCase(requestDto)
You have mocked the service but not injected the mocked service in the rest controller. That's why you are getting a null pointer. So, caseManagementService needs to be injected in CaseController. Below is a link where you can see the injection part. In the below code I have moved caseController variable above so that caseManagementService is injected in caseControler before it is used.
#SpringBootTest
class CaseControllerTests {
#Test
fun `createCase should return case id when a case is created`() {
val caseManagementService: CaseManagementServiceImpl =
Mockito.mock(CaseManagementServiceImpl::class.java)
var caseController = CaseController(caseManagementService)
val caseResponseDtoMono = Mono.just(Fakes().GetFakeCaseResponseDto())
val requestDto = Fakes().GetFakeCaseRequestDto()
`when`(caseManagementService.createAccount(requestDto).thenReturn(caseResponseDtoMono))
//NULL EXCEPTION HAPPENS HERE - RETURNS NULL THIS CALL
var result = caseController.createCase(Fakes().GetFakeCaseRequestDto())
StepVerifier.create(result)
.consumeNextWith { r -> assertEquals(Fakes().GetFakeCaseResponseDto().id, r.id)
}.verifyComplete()
}
}
https://vmaks.github.io/other/2019/11/04/spring-boot-with-mockito-and-kotlin.html
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
}
Project setup
I have a Kotlin Spring Boot 2.0 project that exposes a #RestController API that returns MongoDB models. For example, this model and controller:
#RestController
#RequestMapping("/api/accounts")
class AccountsController() {
#GetMapping
fun list(): List<Account> {
return listOf(Account(ObjectId(), "Account 1"), Account(ObjectId(), "Account 2"), Account(ObjectId(), "Account 3"))
}
}
#Document
data class Account(
#Id val id: ObjectId? = null,
val name: String
)
These models have ObjectId identifiers, but in the API I want them to be treated as plain String (i.e. instead of a complex JSON, the default behaviour).
To achieve this, I created these components to configure Spring Boot parameter binding and JSON parsing:
#JsonComponent
class ObjectIdJsonSerializer : JsonSerializer<ObjectId>() {
override fun serialize(value: ObjectId?, gen: JsonGenerator?, serializers: SerializerProvider?) {
if (value == null || gen == null) return
gen.writeString(value.toHexString())
}
}
#JsonComponent
class ObjectIdJsonDeserializer : JsonDeserializer<ObjectId>() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): ObjectId? {
if (p == null) return null
val text = p.getCodec().readTree<TextNode>(p).textValue()
return ObjectId(text)
}
}
#Component
class StringToObjectIdConverter : Converter<String, ObjectId> {
override fun convert(source: String): ObjectId? {
return ObjectId(source)
}
}
So far this works as intended, calls to the API return this JSON:
[
{
"id": "5da454f4307b0a8b30838839",
"name": "Account 1"
},
{
"id": "5da454f4307b0a8b3083883a",
"name": "Account 2"
},
{
"id": "5da454f4307b0a8b3083883b",
"name": "Account 3"
}
]
Issue
The problem comes when integrating Swagger into the project, the documentation shows that calling this method returns a complex JSON instead of a plain String as the id property:
Adding #ApiModelProperty(dataType = "string") to the id field made no difference, and I can't find a way to solve it without changing all the id fields in the project to String. Any help would be appreciated.
I couldn't get #ApiModelProperty(dataType = "") to work, but I found a more convenient way configuring a direct substitute in the Swagger configuration using directModelSubstitute method of the Docket instance in this response.
#Configuration
#EnableSwagger2
class SwaggerConfig() {
#Bean
fun api(): Docket {
return Docket(DocumentationType.SWAGGER_2)
.directModelSubstitute(ObjectId::class.java, String::class.java)
}
}
Java equivalent:
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.directModelSubstitute(ObjectId.class, String.class);
}
For OpenApi (Swagger 3.0) and SpringDoc the following global configuration could be used.
static {
SpringDocUtils.getConfig().replaceWithSchema(ObjectId.class, new StringSchema());
}
I'm developing a custom logging framework for springboot to log rest-template requests and response and is working fine. Am trying to implement the same for 'Feign-Client' and am faced with couple of issues.
For request logging, am leveraging FeignRequestInterceptor and it is working fine, only problem here is I cannot retrieve the full request URL.
Below method is giving me only relative URL.
requestTemplate.url()
To log the response, only way i could find was the ResponseDecoder. There I'm able to retrieve everything other than the payload. When accessing the payload from
InputStream is = response.body().asInputStream();
String payload = new String(IOUtils.toByteArray(is));
This method works, but the original stream is closed because of which logging happens fine, but client is throwing exception when returning response.
'trying to open closed stream'
I would like suggestions if there are better ways of logging request response in Feign similar to spring rest-template. Or if the method I have adopted is fine, help me resolve the problems above.
You can configure a custom feign.Logger instance to handle this. There are two built in, JavaLogger which uses java.util.logging and Slf4JLogger that uses slf4j. You can create your own logger implementation by extending feign.Logger and registering it as a #Bean.
That logger should be picked up by Spring and registered with your FeignClient. Here is the Logger base class to get you started:
protected abstract void log(String configKey, String format, Object... args);
Create your own instance, implement this method and it will be called before the request and after the response is returned. No need to update the interceptor or create a response decoder.
in your RestConfiguration you need to up default level of logging feignClient and override by #Bean feignLogger like:
#Configuration(proxyBeanMethods = false)
#EnableCircuitBreaker
#EnableFeignClients(basePackageClasses = [Application::class])
class RestConfiguration: WebMvcConfigurer {
#Bean
fun feignLoggerLevel(): Logger.Level {
return Logger.Level.FULL
}
#Bean
fun feignLogger(): Logger {
return FeignClientLogger()
}
}
and implement your logger (logbook format):
import feign.Logger
import feign.Request
import feign.Response
import feign.Util.*
import org.slf4j.LoggerFactory
class FeignClientLogger : Logger() {
private val log = LoggerFactory.getLogger(this::class.java)
override fun logRequest(configKey: String?, logLevel: Level?, request: Request?) {
if (request == null)
return
val feignRequest = FeignRequest()
feignRequest.method = request.httpMethod().name
feignRequest.url = request.url()
for (field in request.headers().keys) {
for (value in valuesOrEmpty(request.headers(), field)) {
feignRequest.addHeader(field, value)
}
}
if (request.requestBody() != null) {
feignRequest.body = request.requestBody().asString()
}
log.trace(feignRequest.toString())
}
override fun logAndRebufferResponse(
configKey: String?,
logLevel: Level?,
response: Response?,
elapsedTime: Long
): Response? {
if (response == null)
return response
val feignResponse = FeignResponse()
val status = response.status()
feignResponse.status = response.status()
feignResponse.reason =
(if (response.reason() != null && logLevel!! > Level.NONE) " " + response.reason() else "")
feignResponse.duration = elapsedTime
if (logLevel!!.ordinal >= Level.HEADERS.ordinal) {
for (field in response.headers().keys) {
for (value in valuesOrEmpty(response.headers(), field)) {
feignResponse.addHeader(field, value)
}
}
if (response.body() != null && !(status == 204 || status == 205)) {
val bodyData: ByteArray = toByteArray(response.body().asInputStream())
if (logLevel.ordinal >= Level.FULL.ordinal && bodyData.isNotEmpty()) {
feignResponse.body = decodeOrDefault(bodyData, UTF_8, "Binary data")
}
log.trace(feignResponse.toString())
return response.toBuilder().body(bodyData).build()
} else {
log.trace(feignResponse.toString())
}
}
return response
}
override fun log(p0: String?, p1: String?, vararg p2: Any?) {}
}
class FeignResponse {
var status = 0
var reason: String? = null
var duration: Long = 0
private val headers: MutableList<String> = mutableListOf()
var body: String? = null
fun addHeader(key: String?, value: String?) {
headers.add("$key: $value")
}
override fun toString() =
"""{"type":"response","status":"$status","duration":"$duration","headers":$headers,"body":$body,"reason":"$reason"}"""
}
class FeignRequest {
var method: String? = null
var url: String? = null
private val headers: MutableList<String> = mutableListOf()
var body: String? = null
fun addHeader(key: String?, value: String?) {
headers.add("$key: $value")
}
override fun toString() =
"""{"type":"request","method":"$method","url":"$url","headers":$headers,"body":$body}"""
}
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)
}