spring type conversion not working - Giving Type Mismatch - spring

I have tried creating a custom converter as specified in - https://www.baeldung.com/spring-type-conversions#creating-a-custom-converter but I am still getting type mismatch when I am carrying out the conversion.
I am creating a converter class (PageToApiResponse) which converts from Page<*> to ResponseList. This converter is then added to registry in the WebConfig class. However when I want to implicitly convert from a Page< ProductDemand > to ResponseList in the controller I get a type mismatch (The service returns Page< ProductDemand >). I have tried annotating the converter class with #Component but that doesn't work.
The error is as follows:
Required: ResponseList.
Found: Page< ProductDemand >
Any help would be greatly appreciated.
Here are the classes involved -
data class ResponseList(
val total: Int,
val limit: Int,
val offset: Int,
val results: List<*>
)
class PageToApiResponse : Converter<Page<*>, ResponseList> {
override fun convert(source: Page<*>) = ResponseList(source.numberOfElements, source.size, source.number, source.content)
}
#Configuration
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
registry.addConverter(PageToApiResponse())
}
}
#ApiOperation(value = "Find demand by GPI")
#ApiResponses(
ApiResponse(code = 200, message = "Demand results", response = ProductDemand::class, responseContainer = "List"),
ApiResponse(code = 400, message = "Bad input parameter"),
ApiResponse(code = 401, message = "Unauthorized"),
ApiResponse(code = 429, message = "Too Many Requests")
)
#PreAuthorize("hasAnyAuthority('${Authorities.EXT_USAGE_VIEW}')")
#GetMapping("")
fun getDemand(
#RequestParam(required = true) gpi: String,
#PageableDefault(
page = 0,
size = 50,
sort = ["molecule"],
direction = Sort.Direction.DESC
) pageable: Pageable
): ResponseList = service.getDemand(gpi, pageable)

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 write test cases for custom ErrorAttributes in spring boot

I have updated the spring boot version to 2.6.4 and related other dependencies and got error in getErrorAttributes() method because of changes in its 2nd arguments type from Boolean to ErrorAttributeOptions
Custom ErrorAtttributes class:
#Component
class CustomErrorAttributes<T : Throwable> :DefaultErrorAttributes() {
override fun getErrorAttributes( request: ServerRequest , options: ErrorAttributeOptions ): MutableMap<String, Any> { // changes made here in 2nd parameter
val errorAttributes = super.getErrorAttributes(request, options) // throwing exception here
val status = (errorAttributes as MutableMap<String,Any>).getOrDefault(STATUS_KEY,null)
if(status != null && status as Int == HttpStatus.INTERNAL_SERVER_ERROR.value()){
errorAttributes.replace(MESSAGE_KEY, INTERNAL_SERVER_ERROR_MESSAGE)
}
return errorAttributes
}
}
Test method
private val internalError = "An unexpected error occurred"
#Mock private lateinit var request : ServerRequest
#Test
fun `For Internal Error`(){
var result : MutableMap<String,Any> = customErrorAttributes.getErrorAttributes(request, options) // It was working earlier version as we pass false in 2nd arguments
assertThat(result["message"]).isEqualTo(internalError)
}

Post Validation didn't work with Spring-Boot Swagger RestAPI

I'm currently working on a Cross Platform App to record the expenses and the incomes. So far so good. However, when I want to add a Backend Post Validation (so nobody can save incomes like €19,99999 etc.) the Post Validation does not work. It would be great if somebody could help me. Here are the details:
I am adding the springfox-bean-validation to my Backend
Entity
#ApiModel(
value = "Expense",
description =
"Information of all expenses with cost, expense date as well as the purpose to which these expenses went"
)
#Entity
#Table(name = "Expenses")
data class Expense(
#ApiModelProperty(notes = "Cost of the Expense in €")
#DecimalMin(value = "0.0", inclusive = false, message = "Price have to be higher than 0.0")
#Digits(integer = Int.MAX_VALUE, fraction = 2, message = "Price have only 2 decimal places")
var cost: BigDecimal,
#ApiModelProperty(notes = "Creation date of the Expense")
#NotNull(message = "Date may not be Null")
var date: LocalDate,
#ApiModelProperty(notes = "Purpose of the expense")
var purpose: String?
) : DbEntity() {
constructor() : this(BigDecimal.ZERO, LocalDate.now(), "")
}
Controller
#CrossOrigin
#RestController
#Api(value = "/finance", description = "FinanceController", produces = "application/json")
#RequestMapping("/api/finance")
class FinanceController(
private val expenseRepository: ExpenseRepository
) {
#ApiOperation(value = "Create an Expense", response = Expense::class)
#PostMapping("/expanse")
fun createExpense(#RequestBody expenseInfo: Expense): Expense {
return expenseRepository.save(expenseInfo)
}
}
SpringFoxConfig
#Configuration
#EnableSwagger2
#Import(BeanValidatorPluginsConfiguration::class)
class SwaggerConfiguration {
#Bean
fun api(): Docket = Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
}
And that is the resulting Output from the swagger UI. Why are there no annotations like #NotNull or #Digits(...)?
When I test a Post request, I get 200 as HTTP Response Code and no Error!
{
"cost": 19.99999999,
"date": "2020-06-06",
"purpose": "test",
"id": 15
}

How to use feign interceptor / decoder to log request - response in custom format?

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}"""
}

Resources