I'm trying to write a Moshi adapter in Kotlin that will create enums based on an integer value, and will also default to a specific enum value if it encounters an unsupported value.
My adapter is never getting called, which causes the default enum adapter to fail with the included stacktrace.
Tracing the code show that the #ToJson annotation somehow isn't present, and so the adapter isn't added when it should be.
Declaring the adapter
enum class Status {
IDLE,
IN_PROGRESS,
FAILED,
SUCCESS,
UNKNOWN;
companion object {
private val map = Status.values().associateBy(Status::ordinal);
fun fromInt(type: Int?): Status {
var ret = map[type]
if (ret == null) {
ret = UNKNOWN
}
return ret
}
}
class Adapter {
#ToJson
fun toJson(status: Status): Int {
return status.ordinal
}
#FromJson
fun fromJson(value: Int): Status {
return fromInt(value)
}
}
}
Building Moshi
#Provides
#Singleton
fun provideMoshi(): Moshi {
return Moshi.Builder()
.add(Status.Adapter())
.add(KotlinJsonAdapterFactory())
.build()
}
Dependencies
implementation 'com.squareup.moshi:moshi:1.6.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.6.0'
Stacktrace
05-29 18:57:56.603 24560-24560/com.example.app E/MyActivity: com.squareup.moshi.JsonDataException: Expected one of [IDLE, IN_PROGRESS, FAILED, SUCCESS, UNKNOWN] but was 2 at path $[0].status
at com.squareup.moshi.StandardJsonAdapters$EnumJsonAdapter.fromJson(StandardJsonAdapters.java:297)
at com.squareup.moshi.StandardJsonAdapters$EnumJsonAdapter.fromJson(StandardJsonAdapters.java:266)
at com.squareup.moshi.JsonAdapter$2.fromJson(JsonAdapter.java:137)
at com.squareup.moshi.ClassJsonAdapter$FieldBinding.read(ClassJsonAdapter.java:196)
at com.squareup.moshi.ClassJsonAdapter.fromJson(ClassJsonAdapter.java:158)
at com.squareup.moshi.JsonAdapter$2.fromJson(JsonAdapter.java:137)
at com.squareup.moshi.CollectionJsonAdapter.fromJson(CollectionJsonAdapter.java:76)
at com.squareup.moshi.CollectionJsonAdapter$2.fromJson(CollectionJsonAdapter.java:53)
at com.squareup.moshi.JsonAdapter$2.fromJson(JsonAdapter.java:137)
at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:45)
at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:27)
...
Related
I have used the memberExtensionProperties() method, but result collection of the extension properties is empty. The test code is attached. What is the right procedure?
class ExtensionPropertyTest {
class DummyClass{}
val DummyClass.id get() = 99
val DummyClass.name get() = "Joe"
#Test
fun testExtensionProperties() {
val dummyClass = DummyClass()
expect(dummyClass.id).toEqual(99) // OK
val properties = DummyClass::class.memberExtensionProperties
.stream()
.toList()
expect(properties).toHaveSize(2) // Fails due a zero size
}
}
memberExtensionProperties does not return extensions over a class, but its members that are at the same time extensions:
fun main() {
println(DummyClass::class.memberExtensionProperties)
}
class DummyClass {
val String.foo: Int
get() = toInt()
}
It is not that easy if at all possible to find all extensions over a class, because extensions are detached from their receivers and they can be located anywhere in the classpath.
A generic class for holding network request result
sealed class Result<out T : Any?> {
data class Success<out T : Any?>(val data: T) : Result<T>()
data class Error(val message: String, val exception: Exception? = null) : Result<Nothing>()
}
A generic function for encapsulating network result into Result.
It is called from a repository and passes a retrofit2 api call as an input parameter
suspend fun <T: Any?> request(method: Call<T>): Result<T> {
return withContext(Dispatchers.IO) {
try {
val response = method.awaitResponse() // Retrofit2 Call
if (response.isSuccessful)
Result.Success(response.body())
else
response.getErrorResult()
} catch (e: Exception) {
Result.Error(e.message.orEmpty(), e)
}
}
// Type mismatch.
// Required: Result<T>
// Found: Result<T?>
}
It is called like this
interface Webservice {
#GET("data")
fun getData(): Call<Data> // Retrofit2
}
suspend fun getData(): Result<Data> {
return request(webservice.getData())
}
Why does it infer the result as type T? but not T?
The problem is this line:
Result.Success(response.body())
body is marked with Nullable, so when ported to Kotlin, response.body() returns a T?, rather than T. See here for how this works.
Therefore, the type parameter for Result.Success is inferred to be T?, and so the expression above creates a Result<T?>. Note that the T in Result<T?> refers to the type parameter of request.
I have an inline function using a reified generic like the following. It is inside of a companion object, therefore static:
inline fun <reified T> getListFromPreferences(preferences : SharedPreferences, key : String)
: MutableList<T> {
return try {
val listAsString = preferences.getString(key, "")
val type: Type = object : TypeToken<List<T>>() {}.type
val gson = SMSApi.gson
gson.fromJson<ArrayList<T>>(listAsString, type)
?: ArrayList()
}catch(exception: JsonSyntaxException) {
ArrayList()
}
}
When I test it with an instrumented test and when I use it in the app itself, it works perfectly fine. However, when I call the function in a background service, it throws a fatal exception, saying it is an illegal type variable reference, quitting the app:
E/AndroidRuntime: FATAL EXCEPTION: Thread-10
Process: example.app, PID: 20728
java.lang.AssertionError: illegal type variable reference
at libcore.reflect.TypeVariableImpl.resolve(TypeVariableImpl.java:111)
at libcore.reflect.TypeVariableImpl.getGenericDeclaration(TypeVariableImpl.java:125)
at libcore.reflect.TypeVariableImpl.hashCode(TypeVariableImpl.java:47)
at com.google.gson.internal.$Gson$Types$WildcardTypeImpl.hashCode($Gson$Types.java:595)
at java.util.Arrays.hashCode(Arrays.java:4074)
at com.google.gson.internal.$Gson$Types$ParameterizedTypeImpl.hashCode($Gson$Types.java:502)
at com.google.gson.reflect.TypeToken.<init>(TypeToken.java:64)
at example.app.NotificationService$remoteNotificationReceived$$inlined$let$lambda$1$1.<init>(PreferenceHelper.kt:16)
at example.app.NotificationService$remoteNotificationReceived$$inlined$let$lambda$1.run(NotificationService.kt:63)
at java.lang.Thread.run(Thread.java:764)
inline fun <reified T> getListFromPreferences(preferences : SharedPreferences, key : String)
: MutableList<T> {
return try {
val listAsString = preferences.getString(key, "")
val type: Type = object : TypeToken<List<T>>() {}.type
val gson = SMSApi.gson
gson.fromJson<ArrayList<T>>(listAsString, type)
?: ArrayList()
}catch(exception: JsonSyntaxException) {
ArrayList()
}
}
The background service is a NotificationService implementing the OSRemoteNotificationReceivedHandler of OneSignal. The function throws the exception in the onNotificationReceived() method.
Is there any reason I donĀ“t understand, why inlining in the application (foreground) is fine, but throws an exception in the background? Or any way to solve this?
EDIT:
Sharing the notificationService, that invokes it:
class NotificationService : OneSignal.OSRemoteNotificationReceivedHandler {
override fun remoteNotificationReceived(context: Context?, notificationReceivedEvent: OSNotificationReceivedEvent?) {
notificationReceivedEvent?.let {
val data = notificationReceivedEvent.notification.additionalData
if(context != null) {
//Fetch some vals
Thread {
val result = //Insert data in db
//-1 will be returned, for rows that are not inserted.
//Rows will not be inserted, if they hurt a unique constraint.
//Therefore the following code should only be executed, when it is inserted.
if(result[0]!=-1L) {
//Get preferences, create item
val list = PreferenceHelper
.getListFromPreferences<MessageAcknowledgement>
(preferences, App.ACKNOWLEDGE_IDS) -> throws error
list.add(acknowledgeMessage)
PreferenceHelper.setListInPreferences(preferences,
App.ACKNOWLEDGE_IDS, list)
//Do some more stuff
}
}.start()
}
Log.d("NotificationService", data.toString())
notificationReceivedEvent.complete(notificationReceivedEvent.notification)
}
}
}
I'm not sure what is the problem with the above code, it would require sharing more of it, but Kotlin has a native way of acquiring Type tokens. Just replace:
object : TypeToken<List<T>>() {}.type
with:
typeOf<List<T>>().javaType
As typeOf() is still experimental, you need to annotate your function with: #OptIn(ExperimentalStdlibApi::class). I use it for some time already and never had any problems, so I guess it is pretty safe to use, at least on JVM.
I have an abstract stub class that needs to be subclasses to add spring annotations.
abstract class ConsumerStub<TYPE>{
val receivedMessages: MutableMap<String, TYPE> = ConcurrentHashMap()
open fun processMessage(#Payload payload: TYPE, record: ConsumerRecord<String, *>) {
this.receivedMessages[record.key()] = payload
}
fun receivedMessageWithKey(key: String): Boolean = this.receivedMessages.contains(key)
fun receivedMessageWithKeyCallable(key: String): Callable<Boolean> = Callable { receivedMessageWithKey(key) }
fun getReceiveMessageWithKey(key: String): TYPE? = this.receivedMessages[key]
fun reset() {
this.receivedMessages.clear()
}
}
for example:
open class WorkflowRequestConsumerStub: ConsumerStub<InternalWorkflowRequest>() {
#KafkaListener(
id = "xyzRequestConsumerStub",
topics = ["abc-workflow-requests"]
)
override fun processMessage(
#Payload payload: InternalWorkflowRequest,
record: ConsumerRecord<String, *>
) {
super.processMessage(payload, record)
}
}
I am seeing some really weird behaviour with the receivedMessages.
After some debugging I realised there seem to be 2 instances of receivedMessages.
stub.reset() throws a null pointer exception
after changing the code to initialize receivedMessages in reset(), processMessage() and receivedMessageWithKey() are seeing 2 different receivedMessages with different objectIds.
what's going on? In java the subclass should have access to any protected members in the super, so I assume the same applies to kotlin.
UPDATE: this works as expected when defining receivedMessages abstract and override in the implementations. That really sucks if this is how it should be done in kotlin. In this case there is no need for the implementation to care about the map.
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.