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
}
Related
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)
},
),
)
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 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.
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.
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();