How to parse response with jackson using ktor? - kotlin

I have this dependencies:
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-jackson:$ktor_version")
implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-client-cio:$ktor_version")
implementation("io.ktor:ktor-client-json:$ktor_version")
And this settings of ktor server:
fun Application.configureHTTP() {
install(DefaultHeaders)
install(CallLogging)
install(AutoHeadResponse)
install(Routing)
install(ContentNegotiation) {
register(ContentType.Application.Json, JacksonConverter())
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
writerWithDefaultPrettyPrinter()
}
}
install(CORS) {
method(HttpMethod.Options)
method(HttpMethod.Put)
method(HttpMethod.Delete)
method(HttpMethod.Patch)
header(HttpHeaders.Authorization)
header("MyCustomHeader")
// allowCredentials = true
anyHost() // #TODO: Don't do this in production if possible. Try to limit it.
}
}
I'd like to get joke's text about Chuck Norris, so I made this data classes:
data class ChuckNorrisJoke(
val type: String,
val value: Map<Any, Any>
)
data class JokeContent(
val id: Long,
val joke: String,
val categories: List<String>
)
And eventually this is my function for getting joke:
val client = HttpClient(CIO) {
install(JsonFeature)
}
suspend fun getChuckNorrisJoke(): ChuckNorrisJoke {
return client
.get("http://api.icndb.com/jokes/random")
}
When I call the method, I get this error:
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.IllegalStateException: Fail to find serializer. Consider to add one of the following dependencies:
- ktor-client-gson
- ktor-client-json
- ktor-client-serialization
at io.ktor.client.features.json.DefaultJvmKt.defaultSerializer(DefaultJvm.kt:14)
at io.ktor.client.features.json.JsonFeature$Feature.prepare(JsonFeature.kt:130)
at io.ktor.client.features.json.JsonFeature$Feature.prepare(JsonFeature.kt:125)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:77)
at io.ktor.client.HttpClientConfig$install$3.invoke(HttpClientConfig.kt:74)
at io.ktor.client.HttpClientConfig.install(HttpClientConfig.kt:97)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:172)
at io.ktor.client.HttpClient.<init>(HttpClient.kt:81)
at io.ktor.client.HttpClientKt.HttpClient(HttpClient.kt:43)
at com.example.ApplicationKt.<clinit>(Application.kt:109)
Can't understand how to set HttpClient correctly.

The implementation("io.ktor:ktor-jackson:$ktor_version") dependency declaration is for the server. You need to declare one for the client: implementation "io.ktor:ktor-client-jackson:$ktor_version". You can find more information here.

Related

How to validate json in kotlin

There is a kotlin class with the following structure.
data class Person(
#field:Length(max = 5)
val name: String,
val phones: List<Phone>
)
data class Phone(
#field:Length(max = 10)
val number: String
)
When converting the json string through objectMapper, I want to receive all the violation values.
ex) JSON object is not valid. Reasons (3) name length must be 5, number length must be 10, ...
#Test
fun test() {
val json = """
{
"name": "name",
"phones": [
{ "number": "1234567890123456" },
{ "number": "1234567890123456" }
]
}
""".trimIndent()
try {
objectMapper.readValue(json, Person::class.java)
} catch (ex: ConstraintViolationException) {
val violations = ex.constraintViolations
println(violations.size) // expected size = 3
}
}
However, the above code fails to catch the exception and causes the exception below.
com.fasterxml.jackson.databind.JsonMappingException: JSON object is not valid. Reasons (1): {"bean":"Phone","property":"number","value":"1234567890123456","message": "..."}, (through reference chain: Person["phones"]->java.util.ArrayList[0])
Looking at the reason, those wrapped in a list do not throw ConstructionViolationException, but throw JsonMappingException.
below dependencies
plugins {
id("org.springframework.boot") version "2.4.3"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.4.30"
kotlin("plugin.spring") version "1.4.30"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
class BeanValidationDeserializer(base: BeanDeserializerBase?) : BeanDeserializer(base) {
private val logger = LoggerFactory.getLogger(this::class.java)
private val validator = Validation.buildDefaultValidatorFactory().validator
override fun deserialize(parser: JsonParser?, ctxt: DeserializationContext?): Any {
val instance = super.deserialize(parser, ctxt)
validate(instance)
return instance
}
private fun validate(instance: Any) {
val violations = validator.validate(instance)
if (violations.isNotEmpty()) {
val message = StringBuilder()
message.append("JSON object is not valid. Reasons (").append(violations.size).append("): ")
for (violation in violations) {
message.append("{\"bean\":\"${violation.rootBeanClass.name}\",")
.append("\"property\":\"${violation.propertyPath}\",")
.append("\"value\":\"${violation.invalidValue}\",")
.append("\"message\": \"${violation.message}\"}")
.append(", ")
}
logger.warn(message.toString())
throw ConstraintViolationException(message.toString(), violations)
}
}
}
#Bean
fun objectMapper(): ObjectMapper = Jackson2ObjectMapperBuilder.json()
.featuresToDisable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.modules(ParameterNamesModule(), JavaTimeModule(), Jdk8Module(), KotlinModule(), customValidationModule())
.build()
#Bean
fun customValidationModule(): SimpleModule {
val validationModule = SimpleModule()
validationModule.setDeserializerModifier(object : BeanDeserializerModifier() {
override fun modifyDeserializer(
config: DeserializationConfig?,
beanDesc: BeanDescription?,
deserializer: JsonDeserializer<*>?
): JsonDeserializer<*>? {
return if (deserializer is BeanDeserializer) {
BeanValidationDeserializer(deserializer as BeanDeserializer?)
} else deserializer
}
})
return validationModule
}
I'm not sure how to do it. I ask for your help.
I would say an easier and more maintainable way would be to define a JSON Schema.
After that is in place, you can use one of the two json validation libraries mentioned here (https://json-schema.org/implementations.html#validator-kotlin) to validate your json.
The answer by #rbs is good but requires the overhead of creating json schema per json you want to validate.
Seems like object mapper can be configured not to wrap the exceptions it throws with JsonMappingException. - https://github.com/FasterXML/jackson-databind/issues/2033
All you need to do is to disable WRAP_EXCEPTIONS feature for the objectmapper
Note you cant choose a specific exception type, it will not wrap ALL the exceptions.

How to solver error for kotlin coroutines flow?

I tried to use NetworkBoundResource for my MVVM Model and after i follow some tutorial, i'm having an error look this
10-21 14:15:04.073 31376-31376/com.example.mvvmsecondmodel E/Process: android_os_Process_getProcessNameByPid pid is 31376
10-21 14:15:04.074 31376-31376/com.example.mvvmsecondmodel E/Process: android_os_Process_getProcessNameByPid value is mvvmsecondmodel
10-21 14:15:04.416 31376-31421/com.example.mvvmsecondmodel E/SQLiteLog: (283) recovered 8 frames from WAL file /data/data/com.example.mvvmsecondmodel/databases/movie_db-wal
10-21 14:15:04.640 31376-31376/com.example.mvvmsecondmodel E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.mvvmsecondmodel, PID: 31376
java.lang.IllegalArgumentException: Unable to create call adapter for kotlinx.coroutines.flow.Flow<com.example.mvvmsecondmodel.data.respository.ApiResponse<com.example.mvvmsecondmodel.data.model.MovieResponse$Movie>>
for method ApiService.getMyMovie
at retrofit2.Utils.methodError(Utils.java:52)
at retrofit2.HttpServiceMethod.createCallAdapter(HttpServiceMethod.java:105)
at retrofit2.HttpServiceMethod.parseAnnotations(HttpServiceMethod.java:66)
at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:37)
at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:170)
at retrofit2.Retrofit$1.invoke(Retrofit.java:149)
at java.lang.reflect.Proxy.invoke(Proxy.java:397)
at $Proxy0.getMyMovie(Unknown Source)
at com.example.mvvmsecondmodel.data.remote.ApiService$DefaultImpls.getMyMovie$default(ApiService.kt:11)
at com.example.mvvmsecondmodel.data.respository.MovieRespository$getListMovie$$inlined$networkBoundResource$1.invokeSuspend(NetworkBoundResource.kt:49)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:55)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Caused by: java.lang.IllegalArgumentException: Could not locate call adapter for kotlinx.coroutines.flow.Flow<com.example.mvvmsecondmodel.data.respository.ApiResponse<com.example.mvvmsecondmodel.data.model.MovieResponse$Movie>>.
Tried:
* retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
* retrofit2.DefaultCallAdapterFactory
at retrofit2.Retrofit.nextCallAdapter(Retrofit.java:241)
at retrofit2.Retrofit.callAdapter(Retrofit.java:205)
at retrofit2.HttpServiceMethod.createCallAdapter(HttpServiceMethod.java:103)
... 14 more
At first i thought there's red sign at my code so i checked ApiService and Respository class and nothing wrong cause i already follow the tutorial
ApiService:
interface ApiService {
#GET("3/movie/popular")
fun getMyMovie(#Query("api_key") api : String = "32bbbffe944d16d1d2a3ee46cfc6aaa0"
) : Flow<ApiResponse<MovieResponse.Movie>>
}
MovieRespository:
class MovieRespository (val apiService: ApiService, val movieDao: MovieDao) {
fun getListMovie() : Flow<Resource<Movie>> {
return networkBoundResource(
fetchFromLocal = { movieDao.getMyMovie() },
shouldFetchFromRemote = {true},
fetchFromRemote = {apiService.getMyMovie()},
processRemoteResponse = {},
saveRemoteData = {movieDao.insert(
it.results.let {
it.map { data -> Movie.from(data) }
}
)},
onFetchFailed = {_, _ ->}
).flowOn(Dispatchers.IO)
}
You should define your api service as suspend function.
Api Service:
interface ApiService {
#GET("3/movie/popular")
suspend fun getMyMovie(#Query("api_key") api : String = "32bbbffe944d16d1d2a3ee46cfc6aaa0"
) : ApiResponse<MovieResponse.Movie>
}
Movie Repository:
class MovieRepository (val apiService: ApiService, val movieDao: MovieDao) {
fun getListMovie() : Flow<Resource<Movie>> {
return flow {
// do your networkBoundResource functions
}.flowOn(Dispatchers.IO)
}
I can't test the code because you didn't share your NetworkBoundResource class. I hope the code help to fix your problem.

Cannot Catch Body From Request

I have an issue with catching data from request body. I know this might be very easy question but I have looking for that in google and ktor docs and havent find solution.
fun main(args: Array<String>) {
val server = embeddedServer(Netty, port = 8080, module = Application::mainModule)
server.start()
}
fun Application.mainModule() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
}
}
routing {
post("/") {
val parameters = call.receiveParameters()
println(parameters)
}
}
}
Sending body in request:
{
"nick": "xxx"
}
Errors:
ERROR ktor.application - Unhandled: POST - /
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `io.ktor.http.Parameters` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (InputStreamReader); line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1589)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1055)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:265)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3218)
at io.ktor.jackson.JacksonConverter.convertForReceive(JacksonConverter.kt:44)
at io.ktor.features.ContentNegotiation$Feature$install$3.invokeSuspend(ContentNegotiation.kt:169)
at io.ktor.features.ContentNegotiation$Feature$install$3.invoke(ContentNegotiation.kt)
at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:318)
at io.ktor.util.pipeline.SuspendFunctionGun.proceed(PipelineContext.kt:163)
try
routing {
post("/") {
val parameters = call.receive<String>()
println(parameters)
}
}
You are using call.receiveParameters, but are passing a json body. =p

Add attribute to XML without POJO modification

I need to supply shared secret attribute to XML, so I decided to add it without exposing it to my API.
#JacksonXmlRootElement(localName = "Request")
data class TestRequest(#JacksonXmlText val request: String)
Here is example POJO, after serializer it looks like
<Request>text</Request>
I need to add attribute to it, like
<Request secret="foobar">text</Request>
I looked to Jackson API and it looks like I need to create custom serializer for root, so
class SessionModule: SimpleModule("Test serializer", PackageVersion.VERSION) {
override fun setupModule(context: SetupContext) {
context.addBeanSerializerModifier(object : XmlBeanSerializerModifier() {
override fun modifySerializer(config: SerializationConfig, beanDesc: BeanDescription, serializer: JsonSerializer<*>): JsonSerializer<*> {
val modifiedSerializer = super.modifySerializer(config, beanDesc, serializer)
if (modifiedSerializer is XmlBeanSerializer) {
println("Registering custom serializer")
return SessionFieldSerializer(modifiedSerializer)
}
return modifiedSerializer
}
})
}
}
And my custom serializer that do nothing
class SessionFieldSerializer: XmlBeanSerializer {
constructor(src: BeanSerializerBase?) : super(src)
constructor(src: XmlBeanSerializerBase?, objectIdWriter: ObjectIdWriter?, filterId: Any?) : super(src, objectIdWriter, filterId)
constructor(src: XmlBeanSerializerBase?, objectIdWriter: ObjectIdWriter?) : super(src, objectIdWriter)
constructor(src: XmlBeanSerializerBase?, toIgnore: MutableSet<String>?) : super(src, toIgnore)
override fun serialize(bean: Any?, g: JsonGenerator?, provider: SerializerProvider?) {
TODO()
}
}
So, all it do is throw not-implemented exception, however even if SessionFieldSerializer() getting instantiated ( I see "Registering custom serializer" message), serialize function is not called.
Test code:
val mapper = XmlMapper()
mapper.registerModule(KotlinModule())
mapper.registerModule(SessionModule())
val request = TestRequest("Foobar")
val test = mapper.writeValueAsString(request)
println(test)
Am I missing something?

Retrofit-Vertx with RxJava2 in Kotlin IllegalStateException message == null

I'm building a very simple application in Kotlin with Vertx and RxJava 2 (RxKotlin), using Kovert REST framework and Retrofit. I have retrofit-vertx adapter and the RxJava2 Retrofit adapter. I can return an arbitrary list from my listUndergroundStations() method, but whenever I try to load from the remote API I get the following error:
Jun 23, 2017 2:16:29 PM uk.amb85.rxweb.api.UndergroundRestController
SEVERE: HTTP CODE 500 - /api/underground/stations - java.io.IOException: java.lang.IllegalStateException: message == null
java.lang.RuntimeException: java.io.IOException: java.lang.IllegalStateException: message == null
at io.reactivex.internal.util.ExceptionHelper.wrapOrThrow(ExceptionHelper.java:45)
at io.reactivex.internal.observers.BlockingMultiObserver.blockingGet(BlockingMultiObserver.java:91)
at io.reactivex.Single.blockingGet(Single.java:2148)
at uk.amb85.rxweb.api.UndergroundRestController$listUndergroundStations$1.invoke(UndergroundRestController.kt:35)
at uk.amb85.rxweb.api.UndergroundRestController$listUndergroundStations$1.invoke(UndergroundRestController.kt:13)
at nl.komponents.kovenant.TaskPromise$wrapper$1.invoke(promises-jvm.kt:138)
at nl.komponents.kovenant.TaskPromise$wrapper$1.invoke(promises-jvm.kt:130)
at nl.komponents.kovenant.NonBlockingDispatcher$ThreadContext.run(dispatcher-jvm.kt:327)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.IOException: java.lang.IllegalStateException: message == null
at com.julienviet.retrofit.vertx.VertxCallFactory$VertxCall.lambda$enqueue$0(VertxCallFactory.java:90)
at io.vertx.core.impl.FutureImpl.tryFail(FutureImpl.java:170)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleException(HttpClientResponseImpl.java:270)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:259)
at io.vertx.core.http.impl.ClientConnection.handleResponseEnd(ClientConnection.java:361)
at io.vertx.core.http.impl.ClientHandler.doMessageReceived(ClientHandler.java:80)
at io.vertx.core.http.impl.ClientHandler.doMessageReceived(ClientHandler.java:38)
at io.vertx.core.http.impl.VertxHttpHandler.lambda$channelRead$0(VertxHttpHandler.java:71)
at io.vertx.core.impl.ContextImpl.lambda$wrapTask$2(ContextImpl.java:335)
at io.vertx.core.impl.ContextImpl.executeFromIO(ContextImpl.java:193)
at io.vertx.core.http.impl.VertxHttpHandler.channelRead(VertxHttpHandler.java:71)
at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:122)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:435)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:293)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:267)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:250)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1228)
at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1039)
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:411)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:248)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:341)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1334)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:363)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:349)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:926)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:129)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:642)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:565)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:479)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:441)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858)
... 1 more
Caused by: java.lang.IllegalStateException: message == null
at okhttp3.Response$Builder.build(Response.java:431)
at com.julienviet.retrofit.vertx.VertxCallFactory$VertxCall.lambda$null$1(VertxCallFactory.java:109)
at io.vertx.core.http.impl.HttpClientResponseImpl$BodyHandler.notifyHandler(HttpClientResponseImpl.java:301)
at io.vertx.core.http.impl.HttpClientResponseImpl.lambda$bodyHandler$0(HttpClientResponseImpl.java:193)
at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:257)
... 36 more
I can't for the life of me work out what is causing the IllegalStateException and have googled it to death. I don't think it's Rx related because I get the same error if I make the method return Observable<List<UndergroundLine>> or even get rid of Rx entirely and return Call<List<UndergroundLine>> (adjusting the controller accordingly). However, beyond that, I'm beating my head against a wall! Is anyone able to point out the error of my ways (besides putting a cushion under my head)?
Main Verticle:
class ApiVerticle : AbstractVerticle() {
override fun start(startFuture: Future<Void>?) {
// Initialise injection.
configureKodein()
val apiRouter = configureRouter(vertx)
vertx.createHttpServer()
.requestHandler { apiRouter.accept(it) }
.listen(8080)
}
private fun configureKodein() {
Kodein.global.addImport(Kodein.Module {
import(TflUndergroundService.module)
})
}
private fun configureRouter(vertx: Vertx): Router {
val apiMountPoint = "api"
val routerInit = fun Router.() {
bindController(UndergroundRestController(), apiMountPoint)
}
val router = Router.router(vertx) initializedBy { router ->
router.routerInit()
}
return router
}
}
TflService:
interface TflService {
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<UndergroundLine>
#GET("/Line/{lineName}/StopPoints")
fun getStationsForUndergroundLine(
#Path("lineName") lineName: String
): Observable<UndergroundStation>
#GET("/Line/{lineName}/Arrivals?stopPointId={stationNaptanId")
fun getArrivalsFor(
#Path("lineName") lineName: String,
#Path("stationNaptanId") stationNaptanId: String
) : Observable<Arrival>
}
data class UndergroundLine(val id: String, val name: String)
data class UndergroundStation(val naptanId: String, val commonName: String)
data class Arrival(
val platformName: String,
val towards: String,
val currentLocation: String,
val expectedArrival: LocalDateTime)
object TflUndergroundService {
val module = Kodein.Module {
val vertx: Vertx = Vertx.currentContext().owner()
val client: HttpClient = vertx.createHttpClient()
val jacksonMapper: ObjectMapper = ObjectMapper()
jacksonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://api.tfl.gov.uk/")
.callFactory(VertxCallFactory(client))
.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
.build()
val tflService: TflService = retrofit.create(TflService::class.java)
bind<TflService>() with instance(tflService)
}
}
ApiKeySecured (Just requires "appid" to be a parameter):
class ApiKeySecured(private val routingContext: RoutingContext) : KodeinGlobalAware {
val user: String = routingContext.request().getParam("appid") ?: throw HttpErrorUnauthorized()
}
The offending REST controller (in Kovert, Promise's are executed on Vertx worker thread):
class UndergroundRestController(val undergroundService: TflService = Kodein.global.instance()) {
fun ApiKeySecured.listUndergroundStations(): Promise<List<UndergroundLine>, Exception> {
//TODO: This is blocking, fix it!??
return task {
undergroundService
.getAllUndergroundLines()
.doOnError { println(it) }
.toList()
.blockingGet()
}
}
}
build.gradle:
mainClassName = "io.vertx.core.Launcher"
def mainVerticleName = "uk.amb85.rxweb.verticles.ApiVerticle"
def configurationFile = "conf/development.json"
run {
args = ["run",
mainVerticleName,
"--launcher-class=$mainClassName",
"-conf $configurationFile"
]
}
There's an issue with retrofit-vertx you are using. OkHttp3's ResponseBuilder requires message to be not null, but VertxCallFactory doesn't set it.
It's fixed in the latest version, but as it's still in development, you have to use snapshot:
repositories {
mavenCentral()
maven {
url "https://oss.sonatype.org/content/repositories/snapshots"
}
}
dependencies {
compile 'com.julienviet:retrofit-vertx:1.0.2-SNAPSHOT'
}
Switching to snapshot dependency fixes the issue you mention in your question, but there's an issue with json mapping, which can be easily fixed by switching code from:
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<UndergroundLine>
to:
#GET("/Line/Mode/tube")
fun getAllUndergroundLines(): Observable<List<UndergroundLine>>
And updating your data classes to have default empty constructor to let Jackson instantiate using reflection:
data class UndergroundLine(var id: String = "", var name: String = "")
More on emtpy default constructor for data classes.
But it's another question related to how to parse response from API you're using to Observable and should be asked if you don't find a workaround.