How to expose data to SSE from a #Tailable query in Spring Data Mongo - spring

After reading the docs of #Tailable of Spring Data MongoDB, I think it is good to use it for message notifications.
#SpringBootApplication
class ServerApplication {
#Bean
fun runner(template: ReactiveMongoTemplate) = CommandLineRunner {
println("running CommandLineRunner...")
template.executeCommand("{\"convertToCapped\": \"messages\", size: 100000}");
}
fun main(args: Array<String>) {
runApplication<ServerApplication>(*args)
}
}
---------
#RestController()
#RequestMapping(value = ["messages"])
#CrossOrigin(origins = ["http://localhost:4200"])
class MessageController(private val messages: MessageRepository) {
#PostMapping
fun hello(p: String) =
this.messages.save(Message(body = p, sentAt = Instant.now())).log().then()
#GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun messageStream(): Flux<Message> = this.messages.getMessagesBy().log()
}
-----------
interface MessageRepository : ReactiveMongoRepository<Message, String> {
#Tailable
fun getMessagesBy(): Flux<Message>
}
------------
#Document(collection = "messages")
data class Message(#Id var id: String? = null, var body: String, var sentAt: Instant = Instant.now())
How to implement it?
Done it by myself, check my solution.

I have resolved this issue myself, check the sample codes.
Also, published a post on medium to demonstrate how to use it a SPA client written in Angular.

Related

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
}

Spring 5 Reactive - WebExceptionHandler is not getting called

I have tried all 3 solutions suggested in what is the right way to handle errors in spring-webflux, but WebExceptionHandler is not getting called. I am using Spring Boot 2.0.0.M7. Github repo here
#Configuration
class RoutesConfiguration {
#Autowired
private lateinit var testService: TestService
#Autowired
private lateinit var globalErrorHandler: GlobalErrorHandler
#Bean
fun routerFunction():
RouterFunction<ServerResponse> = router {
("/test").nest {
GET("/") {
ServerResponse.ok().body(testService.test())
}
}
}
}
#Component
class GlobalErrorHandler() : WebExceptionHandler {
companion object {
private val log = LoggerFactory.getLogger(GlobalErrorHandler::class.java)
}
override fun handle(exchange: ServerWebExchange?, ex: Throwable?): Mono<Void> {
log.info("inside handle")
/* Handle different exceptions here */
when(ex!!) {
is ClientException -> exchange!!.response.statusCode = HttpStatus.BAD_REQUEST
is Exception -> exchange!!.response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR
}
return Mono.empty()
}
}
UPDATE:
When I change Spring Boot version to 2.0.0.M2, the WebExceptionHandler is getting called. Do I need to do something for 2.0.0.M7?
SOLUTION:
As per Brian's suggestion, it worked as
#Bean
#Order(-2)
fun globalErrorHandler() = GlobalErrorHandler()
You can provide your own WebExceptionHandler, but you have to order it relatively to others, otherwise they might handle the error before yours get a chance to try.
the DefaultErrorWebExceptionHandler provided by Spring Boot for error handling (see reference documentation) is ordered at -1
the ResponseStatusExceptionHandler provided by Spring Framework is ordered at 0
So you can add #Order(-2) on your error handling component, to order it before the existing ones.
An error response should have standard payload info. This can be done by extending AbstractErrorWebExceptionHandler
ErrorResponse: Data Class
data class ErrorResponse(
val timestamp: String,
val path: String,
val status: Int,
val error: String,
val message: String
)
ServerResponseBuilder: 2 different methods to build an error response
default: handle standard errors
webClient: handle webClient exceptions (WebClientResponseException), not for this case
class ServerResponseBuilder(
private val request: ServerRequest,
private val status: HttpStatus) {
fun default(): Mono<ServerResponse> =
ServerResponse
.status(status)
.body(BodyInserters.fromObject(ErrorResponse(
Date().format(),
request.path(),
status.value(),
status.name,
status.reasonPhrase)))
fun webClient(e: WebClientResponseException): Mono<ServerResponse> =
ServerResponse
.status(status)
.body(BodyInserters.fromObject(ErrorResponse(
Date().format(),
request.path(),
e.statusCode.value(),
e.message.toString(),
e.responseBodyAsString)))
}
GlobalErrorHandlerConfiguration: Error handler
#Configuration
#Order(-2)
class GlobalErrorHandlerConfiguration #Autowired constructor(
errorAttributes: ErrorAttributes,
resourceProperties: ResourceProperties,
applicationContext: ApplicationContext,
viewResolversProvider: ObjectProvider<List<ViewResolver>>,
serverCodecConfigurer: ServerCodecConfigurer) :
AbstractErrorWebExceptionHandler(
errorAttributes,
resourceProperties,
applicationContext
) {
init {
setViewResolvers(viewResolversProvider.getIfAvailable { emptyList() })
setMessageWriters(serverCodecConfigurer.writers)
setMessageReaders(serverCodecConfigurer.readers)
}
override fun getRoutingFunction(errorAttributes: ErrorAttributes?): RouterFunction<ServerResponse> =
RouterFunctions.route(RequestPredicates.all(), HandlerFunction<ServerResponse> { response(it, errorAttributes) })
private fun response(request: ServerRequest, errorAttributes: ErrorAttributes?): Mono<ServerResponse> =
ServerResponseBuilder(request, status(request, errorAttributes)).default()
private fun status(request: ServerRequest, errorAttributes: ErrorAttributes?) =
HttpStatus.valueOf(errorAttributesMap(request, errorAttributes)["status"] as Int)
private fun errorAttributesMap(request: ServerRequest, errorAttributes: ErrorAttributes?) =
errorAttributes!!.getErrorAttributes(request, false)
}

Partial update REST in Spring Boot and Kotlin

I have a project with Spring Boot + Kotlin + Morphia.
I need add partial update of my entities. My actual post method:
#PostMapping("update/")
fun updateStudent(#RequestBody #Valid student: Student, results: BindingResult): ResponseData<Student> {
if (results.hasErrors())
return ResponseData(errors = results.errors)
if (!student.canEdit(login.user))
return ResponseData()
student.save()
return ResponseData(data = student)
}
I need read student from database and update only the sended fields
This is my solution:
import org.springframework.beans.BeanWrapperImpl
import java.util.HashSet
fun getNullPropertyNames(source: Any): Array<String> {
val src = BeanWrapperImpl(source)
val pds = src.propertyDescriptors
val emptyNames = HashSet<String>()
for (pd in pds) {
if (src.getPropertyValue(pd.name) == null) emptyNames.add(pd.name)
}
return emptyNames.toTypedArray()
}
And in controller
import org.springframework.beans.BeanUtils
#RestController
class GateController {
#Autowired
private val modelRepository: MyRepository? = null
// allow both 'full' and 'partial' update
#PutMapping("/somemodel/{Id}")
fun updateModel(
#PathVariable Id: Long,
#RequestBody requestBody: SomeModel
): SomeModel {
var objFromDb = modelRepository!!.findById(Id).orElseThrow { ResourceNotFoundException("Object not found with id: " + Id) }
BeanUtils.copyProperties(requestBody, objFromDb, *getNullPropertyNames(requestBody))
return modelRepository.save(objFromDb)
}
...
}
There are 2 things to implement. Reading Student from DB and copying properties from the student from request.
I post java code but it's no problem to convert to kotlin
Morphia morphia = new Morphia();
db = new Mongo();
Datastore ds = morphia.createDatastore(db, appname, user, pass.toCharArray());
morphia.map(Student.class);
Student existing= ds.find(Student.class).field("id").equal(student.id).get();
Then you can use Apache BeanUtils
http://commons.apache.org/proper/commons-beanutils/javadocs/v1.8.3/apidocs/org/apache/commons/beanutils/BeanUtils.html#copyProperties%28java.lang.Object,%20java.lang.Object%29
BeanUtils.copyProperties(existing, student);
then
existing.save();

Resources