How to combine key and value using Jackson (Spring boot) - spring-boot

I have json like below.
{
"USER0001": {
"name": "hoge",
"age": 20
},
"USER0002": {
"name": "huga",
"age": 10
}
}
and, this is my User data class.
data class User(
val id: String,
val name: String,
val age: Int
)
then, I want to convert json to user list when request is send controller.
listOf(
User("USER0001", "hoge", 20),
User("USER0002", "huga", 10),
)
and my controller .
#RestController
class MyController() {
fun test(#RequestBody users: List<User>) {
// some code. I want to use users as List<User>
}
}
I try using #JsonComponent like below,
class Deserializer : JsonDeserializer<List<User>>() {
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): List<User> {
val treeNode = parser.codec.readTree<TreeNode>(parser)
val fieldNames = treeNode.fieldNames()
val result = mutableListOf<User>()
while(fieldNames.hasNext()) {
val fieldName = fieldNames.next()
val userJson = treeNode.get(fieldName)
// I can't use this code as String type.
val name = userJson.get("name")
// How Can I make User model ???
}
return result
}
}
then, I don't know how to make User object in deserializer method.
do you know how to do this ?
thank you reading.

this is simple way.
#RestController
class MyController() {
fun test(#RequestBody Map<String, UserDetail>) {
// some code...
}
}

Related

How can I infer generic type in kotlin? (covariance problem)

I have two class which extends Data, interface.
A: Data
B: Data
Then I have two repositories. TestRepository is interface which get generic class.
TestRepository<T: Data> {
fun save(data: T): T
}
#Repository
ARepository: TestRepository<A> {
override fun save(data: A): A
}
#Repository
BRepository: TestRepository<B> {
override fun save(data: B): B
}
it all have save method which gets data from generic type, and returns generic type.
ARepo and BRepo gets data from A: Data, B:Data and returns corresponding type.
Then we have new Service,
#Service
CService(
private aRepository: ARepository,
private bRepository: BRepository
) {
fun test(t: String): TestRepository<out Data> =
when (t) {
'1' -> aRepository
'2' -> bRepository
else -> throw Error("error")
}
}
it returns aRepository or bRepository, so return type of test function is TestRepository<out Data>. But when I try to use that class with DI,
#Service
class TestClass(
private val cService: CService
) {
fun cServiceTest() {
...
val saveObject = Data('')
val repo = cService.test("1") // or "2"
repo.save(saveObject) <-- error
}
}
repo.save emits error,
Type mismatch.
Required:
Nothing
Found:
Data
How can I solve this error?
How about this?
import kotlin.reflect.KClass
interface Data {
val name: String
}
data class A(
override val name: String = "A"
) : Data
data class B(
override val name: String = "B"
) : Data
interface DataRepository<T : Data> {
fun save(data: T): T
}
class ARepository : DataRepository<A> {
override fun save(data: A): A {
println("Saved - (A)")
return data.copy()
}
}
class BRepository : DataRepository<B> {
override fun save(data: B): B {
println("Saved - (B)")
return data.copy()
}
}
class DataService(
private val aClassRepository: ARepository = ARepository(),
private val bClassRepository: BRepository = BRepository(),
) {
#Suppress("UNCHECKED_CAST")
fun <T : Data> getRepository(_class: KClass<T>): DataRepository<T> =
when (_class) {
A::class -> aClassRepository
B::class -> bClassRepository
else -> throw RuntimeException()
} as? DataRepository<T> ?: throw RuntimeException()
}
fun main() {
val service = DataService()
val repository = service.getRepository(A::class)
val saved = repository.save(A())
println(saved)
}
I don't know what kind of problem you are trying to solve.
So I think the solution I came up with is not good.

Using ConnectableFlux for hot stream REST endpoint

I'm trying to create a REST endpoint to subscribe to a hot stream using Reactor.
My test provider for the stream looks like this:
#Service
class TestProvider {
fun getStream(resourceId: String): ConnectableFlux<QData> {
return Flux.create<QData> {
for (i in 1..10) {
it.next(QData(LocalDateTime.now().toString(), "next"))
Thread.sleep(500L)
}
it.complete()
}.publish()
}
}
My service for the REST endpoint looks like this:
#Service
class DataService #Autowired constructor(
private val prv: TestProvider
) {
private val streams = mutableMapOf<String, ConnectableFlux<QData>>()
fun subscribe(resourceId: String): Flux<QData> {
val stream = getStream(resourceId)
return Flux.push { flux ->
stream.subscribe{
flux.next(it)
}
flux.complete()
}
}
private fun getStream(resourceId: String): ConnectableFlux<QData> {
if(streams.containsKey(resourceId).not()) {
streams.put(resourceId, createStream(resourceId))
}
return streams.get(resourceId)!!
}
private fun createStream(resourceId: String): ConnectableFlux<QData> {
val stream = prv.getStream(resourceId)
stream.connect()
return stream
}
}
The controller looks like this:
#RestController
class DataController #Autowired constructor(
private val dataService: DataService
): DataApi {
override fun subscribe(resourceId: String): Flux<QData> {
return dataService.subscribe(resourceId)
}
}
The API interface looks like this:
interface DataApi {
#ApiResponses(value = [
ApiResponse(responseCode = "202", description = "Client is subscribed", content = [
Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = QData::class)))
])
])
#GetMapping(path = ["/subscription/{resourceId}"], produces = [MediaType.APPLICATION_JSON_VALUE])
fun subscribe(
#Parameter(description = "The resource id for which quality data is subscribed for", required = true, example = "example",allowEmptyValue = false)
#PathVariable("resourceId", required = true) #NotEmpty resourceId: String
): Flux<QData>
}
Unfortunately, my curl delivers an empty array.
Does anyone has an idea what the issue is? Thanks in advance!
I had to run connect() async in DataService:
CompletableFuture.runAsync {
stream.connect()
}

Spring HATEOAS RepresentationModelAssembler to generate links with Pageable parameter

I have a MemberController that has two GetMappings, one returns a paginated list of members and the other returns a member. I have a MemberModelAssembler which overrides toModel and returns a selfRel() link. How to make the toModel method in the MemberModelAssembler return a the pagination link for each member? Given I cannot pass Pageable and PagedResourcesAssembler to the MemberModelAssembler?
Expected result when calling api/v1/member/1
{
"id": 1,
"phone": "85298890006",
"profileImageUrl": null,
"displayedName": "Mak",
"salutation": "MS",
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/member/1"
}
*****Want to achieve this*****
"members": {
"href": "http://localhost:8080/api/v1/memberpage=0&size=20"
*****Want to achieve this*****
}
}
My MemberController:
#RestController
#RequestMapping("api/v1/member")
class MemberController(
private val service: MemberService,
private val assembler: MemberModelAssembler
) {
#GetMapping
fun findAll(
pageable: Pageable,
pagedResourcesAssembler: PagedResourcesAssembler<Member>
): ResponseEntity<PagedModel<EntityModel<Member>>> {
val members = service.findAll(pageable)
return ResponseEntity(pagedResourcesAssembler.toModel(members, assembler), HttpStatus.OK)
}
#GetMapping("/{id}")
fun findById(#PathVariable id: Int): ResponseEntity<EntityModel<Member>> {
val member = service.findById(id) ?: throw ItemNotFoundException(this::class.simpleName!!, id)
return ResponseEntity(assembler.toModel(member), HttpStatus.OK)
}
}
My MemberModelAssembler
#Component
class MemberModelAssembler : RepresentationModelAssembler<Member, EntityModel<Member>> {
override fun toModel(member: Member) =
EntityModel.of(
member,
linkTo(methodOn(MemberController::class.java).findById(member.id)).withSelfRel(),
)
}
For anyone who still has a problem with that:
Although it's a little counteractive, you can enter a 3rd parameter as the initial link.

Blocking problem of graphql-kotlin with DataLoader&BatchLoader

I used the framework "https://github.com/ExpediaGroup/graphql-kotlin" to learn graphql programming of kotlin under springframework.
I used DataLoader&BatchLoader to resolve the 'N+1' loading problem.
When the scope of DataLoader objects is singleton, it works, but it's not my goal because temporary cache mechanism should not be bridging over different requests.
Then I changed the scope of DataLoader objects to be prototype, in all probability, the graphql query may be blocking and associated objects will not be loaded, client waits the response forever.
What's the reason and how can I resolve it?
I did it like this:
Create a simple springboot application, add maven dependency of graph-kotlin
<dependency>
<groupId>com.expediagroup</groupId>
<artifactId>graphql-kotlin-spring-server</artifactId>
<version>2.0.0.RC3</version>
</dependency>
Create two model classes(Note: Their code will be changed in the final step)
data class Department(
val id: Long,
val name: String
)
data class Employee(
val id: Long,
val name: String,
#GraphQLIgnore val departmentId: Long
)
Create two mocked repository objects
val DEPARTMENTS = listOf(
Department(1L, "Develop"),
Department(2L, "Test")
)
val EMPLOYEES = listOf(
Employee(1L, "Jim", 1L),
Employee(2L, "Kate", 1L),
Employee(3L, "Tom", 2L),
Employee(4L, "Mary", 2L)
)
#Repository
open class DepartmentRepository {
companion object {
private val LOGGER = LoggerFactory.getLogger(DepartmentRepository::class.java)
}
open fun findByName(namePattern: String?): List<Department> = //For root query
namePattern
?.takeIf { it.isNotEmpty() }
?.let { pattern ->
DEPARTMENTS.filter { it.name.contains(pattern) }
}
?: DEPARTMENTS
open fun findByIds(ids: Collection<Long>): List<Department> { // For assciation
LOGGER.info("BatchLoad departments by ids: [${ids.joinToString(", ")}]")
return DEPARTMENTS.filter { ids.contains(it.id) }
}
}
#Repository
open class EmployeeRepository {
companion object {
private val LOGGER = LoggerFactory.getLogger(EmployeeRepository::class.java)
}
open fun findByName(namePattern: String?): List<Employee> = //For root query
namePattern
?.takeIf { it.isNotEmpty() }
?.let { pattern ->
EMPLOYEES.filter { it.name.contains(pattern) }
}
?: EMPLOYEES
open fun findByDepartmentIds(departmentIds: Collection<Long>): List<Employee> { // For association
LOGGER.info("BatchLoad employees by departmentIds: [${departmentIds.joinToString(", ")}]")
return EMPLOYEES.filter { departmentIds.contains(it.departmentId) }
}
}
Create a graphql query object to export root query operations
#Service
open class OrgService(
private val departmentRepository: DepartmentRepository,
private val employeeRepository: EmployeeRepository
) : Query {
fun departments(namePattern: String?): List<Department> =
departmentRepository.findByName(namePattern)
fun employees(namePattern: String?): List<Employee> =
employeeRepository.findByName(namePattern)
}
Create an abstract class for many-to-one associated object loading
abstract class AbstractReferenceLoader<K, R: Any> (
batchLoader: (Collection<K>) -> Collection<R>,
keyGetter: (R) ->K,
optionsInInitializer: (DataLoaderOptions.() -> Unit) ? = null
): DataLoader<K, R?>(
{ keys ->
CompletableFuture.supplyAsync {
batchLoader(keys)
.associateBy(keyGetter)
.let { map ->
keys.map { map[it] }
}
}
},
optionsInInitializer?.let {
DataLoaderOptions().apply {
this.it()
}
}
)
Create an abstract class for one-to-many associated collection loading
abstract class AbstractListLoader<K, E>(
batchLoader: (Collection<K>) -> Collection<E>,
keyGetter: (E) ->K,
optionsInInitializer: (DataLoaderOptions.() -> Unit) ? = null
): DataLoader<K, List<E>>(
{ keys ->
CompletableFuture.supplyAsync {
batchLoader(keys)
.groupBy(keyGetter)
.let { map ->
keys.map { map[it] ?: emptyList() }
}
}
},
optionsInInitializer?.let {
DataLoaderOptions().apply {
this.it()
}
}
)
Create annotation to let spring manager DataLoader beans by prototype scope
#Retention(RetentionPolicy.RUNTIME)
#Target(AnnotationTarget.CLASS)
#Component
#Scope(
ConfigurableBeanFactory.SCOPE_PROTOTYPE,
proxyMode = ScopedProxyMode.NO
)
annotation class DataLoaderComponent
Create loader bean to load the parent object reference of Employee object
#DataLoaderComponent
open class DepartmentLoader(
private val departmentRepository: DepartmentRepository
): AbstractReferenceLoader<Long, Department>(
{ departmentRepository.findByIds(it) },
{ it.id },
{ setMaxBatchSize(256) }
)
Create loader bean to load the child object collection of Department object
#DataLoaderComponent
open class EmployeeListByDepartmentIdLoader(
private val employeeRepository: EmployeeRepository
): AbstractListLoader<Long, Employee>(
{ employeeRepository.findByDepartmentIds(it) },
{ it.departmentId },
{ setMaxBatchSize(16) }
)
Create an GraphQL configuration to let 'graphql-kotlin' know all the DataLoader beans
#Configuration
internal abstract class GraphQLConfig {
#Bean
open fun dataLoaderRegistryFactory(): DataLoaderRegistryFactory =
object: DataLoaderRegistryFactory {
override fun generate(): DataLoaderRegistry = dataLoaderRegistry()
}
#Bean
#Scope(
ConfigurableBeanFactory.SCOPE_PROTOTYPE,
proxyMode = ScopedProxyMode.NO
)
protected open fun dataLoaderRegistry(
loaders: List<DataLoader<*, *>>
): DataLoaderRegistry =
DataLoaderRegistry().apply {
loaders.forEach { loader ->
register(
loader::class.qualifiedName,
loader
)
}
}
#Lookup
protected abstract fun dataLoaderRegistry(): DataLoaderRegistry
}
Add tow methods into DataFetchingEnvironment to get DataLoader objects
inline fun <reified L: AbstractReferenceLoader<K, R>, K, R> DataFetchingEnvironment.getReferenceLoader(
): DataLoader<K, R?> =
this.getDataLoader<K, R?>(L::class.qualifiedName)
inline fun <reified L: AbstractListLoader<K, E>, K, E> DataFetchingEnvironment.getListLoader(
): DataLoader<K, List<E>> =
this.getDataLoader<K, List<E>>(L::class.qualifiedName)
Change the code of Department and Employee, let them support association
data class Department(
val id: Long,
val name: String
) {
suspend fun employees(env: DataFetchingEnvironment): List<Employee> =
env
.getListLoader<EmployeeListByDepartmentIdLoader, Long, Employee>()
.load(id)
.await()
}
data class Employee(
val id: Long,
val name: String,
#GraphQLIgnore val departmentId: Long
) {
suspend fun department(env: DataFetchingEnvironment): Department? =
env
.getReferenceLoader<DepartmentLoader, Long, Department>()
.load(departmentId)
.await()
}
Build & Run
Start the SpringBoot applications, open http://locathost:8080/playground.
Execute the query, the result may be success or failed!
{
employees {
id
name
department {
id
name
employees {
id
name
}
}
}
}
If it is success, the client can get the response, and the server log is
2020-03-15 22:47:26.366 INFO 35616 --- [onPool-worker-5] org.frchen.dal.DepartmentRepository : BatchLoad departments by ids: [1, 2]
2020-03-15 22:47:26.367 INFO 35616 --- [onPool-worker-5] org.frchen.dal.EmployeeRepository : BatchLoad employees by departmentIds: [1, 2]
If it is failed, the client is blocking and waits for the response forever, and the server log is
2020-03-15 22:53:43.159 INFO 35616 --- [onPool-worker-6] org.frchen.dal.DepartmentRepository : BatchLoad departments by ids: [1, 2]

Spring RepositoryRestController with excerptProjection

I have defined a #Projection for my Spring Data entity as described here
For the same reasons as described there. When I do GET request, everything is returned as expected. But when I do a POST request, the projection won't work. Following the example provided above, "Address" is shown as a URL under Links and is not exposed the way it is with GET request.
How to get it exposed the same way?
I created a class with #RepositoryRestController where I can catch the POST method. If I simply return the entity, it is without links. If I return it as a resource, the links are there, but "Address" is also a link. If I remove the GET method from my controller, the default behavior is as described above.
UPDATE
My entities are same as described here A, B and SuperClass except I don't have fetch defined in my #ManyToOne
My controller looks like this:
#RepositoryRestController
public class BRepositoryRestController {
private final BRepository bRepository;
public BRepositoryRestController(BRepository bRepository) {
this.bRepository = bRepository;
}
#RequestMapping(method = RequestMethod.POST, value = "/bs")
public
ResponseEntity<?> post(#RequestBody Resource<B> bResource) {
B b= bRepository.save(bResource.getContent());
BProjection result = bRepository.findById(b.getId());
return ResponseEntity.ok(new Resource<>(result));
}
}
And my repository looks like this:
#RepositoryRestResource(excerptProjection = BProjection.class)
public interface BRepository extends BaseRepository<B, Long> {
#EntityGraph(attributePaths = {"a"})
BProjection findById(Long id);
}
And my projection looks like this:
#Projection(types = B.class)
public interface BProjection extends SuperClassProjection {
A getA();
String getSomeData();
String getOtherData();
}
And SuperClassProjection looks like this:
#Projection(types = SuperClass.class)
public interface SuperClassProjection {
Long getId();
}
In the custom #RepositoryRestController POST method you should also return the projection. For example:
#Projection(name = "inlineAddress", types = { Person.class })
public interface InlineAddress {
String getFirstName();
String getLastName();
#Value("#{target.address}")
Address getAddress();
}
public interface PersonRepo extends JpaRepository<Person, Long> {
InlineAddress findById(Long personId);
}
#PostMapping
public ResponseEntity<?> post(...) {
//... posting a person
InlineAddress inlineAddress = bookRepo.findById(person.getId());
return ResponseEntity.ok(new Resource<>(inlineAddress));
}
UPDATE
I've corrected my code above and the code from the question:
#RepositoryRestResource(excerptProjection = BProjection.class)
public interface BRepository extends CrudRepository<B, Long> {
BProjection findById(Long id);
}
#Projection(types = B.class)
public interface BProjection {
#Value("#{target.a}")
A getA();
String getSomeData();
String getOtherData();
}
Then all works fine.
POST request body:
{
"name": "b1",
"someData": "someData1",
"otherData": "otherData",
"a": {
"name": "a1"
}
}
Response body:
{
"a": {
"name": "a1"
},
"someData": "someData1",
"otherData": "otherData",
"_links": {
"self": {
"href": "http://localhost:8080/api/bs/1{?projection}",
"templated": true
}
}
}
See working example

Resources