1
0
mirror of https://github.com/ultrasonic/ultrasonic synced 2025-02-02 18:26:49 +01:00

Switched to using Jackson for json parsing.

Also changed how response is parsed, allowing base response has different models to parse.

Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
This commit is contained in:
Yahor Berdnikau 2017-04-01 18:40:14 +02:00
parent 0dd01d18ba
commit cfa90e0a8d
14 changed files with 193 additions and 100 deletions

View File

@ -4,17 +4,18 @@ ext.versions = [
compileSdk : 22,
buildTools : "25.0.2",
androidTools : "2.2.3",
androidTools : "2.3.0",
androidSupport : "22.2.1",
kotlin : "1.0.6",
kotlin : "1.1.0",
retrofit : "2.1.0",
jackson : "2.8.7",
junit : "4.12",
mockitoKotlin : "1.3.0",
kluent : "1.14",
kluent : "1.15",
okhttp : "3.6.0",
]
@ -28,14 +29,12 @@ ext.androidSupport = [
design : "com.android.support:design:$versions.androidSupport",
]
ext.kotlin = [
stdlib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin"
]
ext.other = [
kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib-common:$versions.kotlin",
retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
gsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit",
simpleXmlConverter : "com.squareup.retrofit2:converter-simplexml:$versions.retrofit",
jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit",
jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson",
]
ext.testing = [

View File

@ -8,10 +8,10 @@ sourceSets {
}
dependencies {
compile kotlin.stdlib
compile other.kotlinStdlib
compile other.retrofit
compile other.gsonConverter
compile other.jacksonConverter
compile other.jacksonKotlin
testCompile testing.junit
testCompile testing.kotlinJunit

View File

@ -3,14 +3,18 @@ package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse
import okio.Okio
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.SubsonicResponse
import org.moire.ultrasonic.api.subsonic.models.License
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
import retrofit2.Response
import java.nio.charset.Charset
import java.text.SimpleDateFormat
import java.util.*
/**
* Integration test for [SubsonicAPI] class.
@ -72,6 +76,21 @@ class SubsonicAPITest {
with(response.body()) {
status `should be` SubsonicResponse.Status.OK
version `should be` SubsonicAPIVersions.V1_13_0
license `should equal` License(true, parseDate("2016-11-23T20:17:15.206Z"))
}
}
@Test
fun `Should parse error get license response`() {
enqueueResponse("generic_error_response.json")
val response = api.getApi().getLicense().execute()
assertResponseSuccessful(response)
with(response.body()) {
status `should be` SubsonicResponse.Status.ERROR
error `should be` SubsonicError.GENERIC
license `should be` null
}
}
@ -85,8 +104,16 @@ class SubsonicAPITest {
return source.readString(Charset.forName("UTF-8"))
}
private fun assertResponseSuccessful(response: Response<SubsonicResponse>) {
private fun <T> assertResponseSuccessful(response: Response<T>) {
response.isSuccessful `should be` true
response.body() `should not be` null
}
private fun parseDate(dateAsString: String): Calendar {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US)
val result = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
result.time = dateFormat.parse(dateAsString.replace("Z$".toRegex(), "+0000"))
return result
}
}

View File

@ -1,10 +1,11 @@
package org.moire.ultrasonic.api.subsonic
import com.google.gson.GsonBuilder
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import okhttp3.OkHttpClient
import org.moire.ultrasonic.api.subsonic.models.SubsonicResponse
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.jackson.JacksonConverterFactory
import java.math.BigInteger
/**
@ -31,15 +32,14 @@ class SubsonicAPI(baseUrl: String,
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
}.build()
private val gson = GsonBuilder()
.registerTypeAdapter(SubsonicResponse::class.javaObjectType,
SubsonicResponse.Companion.ClassTypeAdapter())
.create()
private val jacksonMapper = ObjectMapper()
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
.registerModule(KotlinModule())
private val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
.build()
private val subsonicAPI = retrofit.create(SubsonicAPIDefinition::class.java)

View File

@ -1,6 +1,7 @@
package org.moire.ultrasonic.api.subsonic
import org.moire.ultrasonic.api.subsonic.models.SubsonicResponse
import org.moire.ultrasonic.api.subsonic.response.LicenseResponse
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import retrofit2.Call
import retrofit2.http.GET
@ -12,5 +13,5 @@ interface SubsonicAPIDefinition {
fun ping(): Call<SubsonicResponse>
@GET("getLicense.view")
fun getLicense(): Call<SubsonicResponse>
fun getLicense(): Call<LicenseResponse>
}

View File

@ -1,8 +1,15 @@
package org.moire.ultrasonic.api.subsonic
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
/**
* Subsonic REST API versions.
*/
@JsonDeserialize(using = SubsonicAPIVersions.Companion.SubsonicAPIVersionsDeserializer::class)
enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion: String) {
V1_1_0("3.8", "1.1.0"),
V1_1_1("3.9", "1.1.1"),
@ -41,5 +48,14 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
}
throw IllegalArgumentException("Unknown api version $apiVersion")
}
class SubsonicAPIVersionsDeserializer: JsonDeserializer<SubsonicAPIVersions>() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): SubsonicAPIVersions {
if (p!!.currentName != "version") {
throw JsonParseException(p, "Not valid token for API version!")
}
return fromApiVersion(p.text)
}
}
}
}

View File

@ -1,8 +1,14 @@
package org.moire.ultrasonic.api.subsonic
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
/**
* Common API errors.
*/
@JsonDeserialize(using = SubsonicError.Companion.SubsonicErrorDeserializer::class)
enum class SubsonicError(val code: Int) {
GENERIC(0),
REQUIRED_PARAM_MISSING(10),
@ -18,5 +24,15 @@ enum class SubsonicError(val code: Int) {
fun parseErrorFromJson(jsonErrorCode: Int) = SubsonicError.values()
.filter { it.code == jsonErrorCode }.firstOrNull()
?: throw IllegalArgumentException("Unknown code $jsonErrorCode")
class SubsonicErrorDeserializer: JsonDeserializer<SubsonicError>() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): SubsonicError {
p!!.nextToken() // "code"
val error = parseErrorFromJson(p.valueAsInt)
p.nextToken() // "message"
p.nextToken() // end of error object
return error
}
}
}
}

View File

@ -0,0 +1,5 @@
package org.moire.ultrasonic.api.subsonic.models
import java.util.*
data class License(val valid: Boolean, val trialExpires: Calendar)

View File

@ -1,73 +0,0 @@
package org.moire.ultrasonic.api.subsonic.models
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicError
/**
* Base Subsonic API response.
*/
data class SubsonicResponse(val status: Status,
val version: SubsonicAPIVersions,
val error: SubsonicError?) {
enum class Status(val jsonValue: String) {
OK("ok"), ERROR("failed");
companion object {
fun getStatusFromJson(jsonValue: String) = Status.values()
.filter { it.jsonValue == jsonValue }.firstOrNull()
?: throw IllegalArgumentException("Unknown status value: $jsonValue")
}
}
companion object {
class ClassTypeAdapter: TypeAdapter<SubsonicResponse>() {
override fun read(`in`: JsonReader?): SubsonicResponse {
if (`in` == null) {
throw NullPointerException("No json for parsing")
}
var status: Status = Status.ERROR
var version: SubsonicAPIVersions = SubsonicAPIVersions.V1_1_0
var error: SubsonicError? = null
`in`.beginObject()
if ("subsonic-response" == `in`.nextName()) {
`in`.beginObject()
while (`in`.hasNext()) {
when (`in`.nextName()) {
"status" -> status = Status.getStatusFromJson(`in`.nextString())
"version" -> version = SubsonicAPIVersions.fromApiVersion(`in`.nextString())
"error" -> error = parseError(`in`)
else -> `in`.skipValue()
}
}
`in`.endObject()
} else{
throw IllegalArgumentException("Not a subsonic-response json!")
}
`in`.endObject()
return SubsonicResponse(status, version, error)
}
override fun write(out: JsonWriter?, value: SubsonicResponse?) {
throw UnsupportedOperationException("not implemented")
}
private fun parseError(reader: JsonReader): SubsonicError? {
var error: SubsonicError? = null
reader.beginObject()
while (reader.hasNext()) {
when (reader.nextName()) {
"code" -> error = SubsonicError.parseErrorFromJson(reader.nextInt())
else -> reader.skipValue()
}
}
reader.endObject()
return error
}
}
}
}

View File

@ -0,0 +1,12 @@
package org.moire.ultrasonic.api.subsonic.response
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicError
import org.moire.ultrasonic.api.subsonic.models.License
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
class LicenseResponse(val license: License?,
status: Status,
version: SubsonicAPIVersions,
error: SubsonicError?):
SubsonicResponse(status, version, error)

View File

@ -0,0 +1,39 @@
package org.moire.ultrasonic.api.subsonic.response
import com.fasterxml.jackson.annotation.JsonRootName
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicError
/**
* Base Subsonic API response.
*/
@JsonRootName(value = "subsonic-response")
open class SubsonicResponse(val status: Status,
val version: SubsonicAPIVersions,
val error: SubsonicError?) {
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
enum class Status(val jsonValue: String) {
OK("ok"), ERROR("failed");
companion object {
fun getStatusFromJson(jsonValue: String) = values()
.filter { it.jsonValue == jsonValue }.firstOrNull()
?: throw IllegalArgumentException("Unknown status value: $jsonValue")
class StatusJsonDeserializer: JsonDeserializer<Status>() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Status {
if (p!!.currentName != "status") {
throw JsonParseException(p,
"Current token is not status. Current token name ${p.currentName}.")
}
return getStatusFromJson(p.text)
}
}
}
}
}

View File

@ -0,0 +1,44 @@
package org.moire.ultrasonic.api.subsonic
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should throw`
import org.junit.Before
import org.junit.Test
/**
* Unit test for [SubsonicAPIVersions.SubsonicAPIVersionsDeserializer] class.
*/
class SubsonicAPIVersionsDeserializerTest {
private val jsonParser = mock<JsonParser>()
private val context = mock<DeserializationContext>()
private lateinit var deserializer: SubsonicAPIVersions.Companion.SubsonicAPIVersionsDeserializer
@Before
fun setUp() {
deserializer = SubsonicAPIVersions.Companion.SubsonicAPIVersionsDeserializer()
}
@Test
fun `Should throw if current token name is not version`() {
doReturn("asdasd").whenever(jsonParser).currentName
{ deserializer.deserialize(jsonParser, context) } `should throw` JsonParseException::class
}
@Test
fun `Should return parsed version`() {
doReturn("version").whenever(jsonParser).currentName
doReturn(SubsonicAPIVersions.V1_13_0.restApiVersion).whenever(jsonParser).text
val parsedVersion = deserializer.deserialize(jsonParser, context)
parsedVersion `should be` SubsonicAPIVersions.V1_13_0
}
}

View File

@ -4,6 +4,7 @@ import org.amshove.kluent.`should equal`
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
/**
* Unit test for [SubsonicResponse.Status] class

View File

@ -32,10 +32,16 @@ dependencies {
compile androidSupport.support
compile androidSupport.design
compile kotlin.stdlib
compile other.kotlinStdlib
testCompile testing.junit
testCompile testing.kotlinJunit
testCompile testing.mockitoKotlin
testCompile testing.kluent
testCompile(testing.kotlinJunit) {
exclude module: "kotlin-stdlib"
}
testCompile(testing.mockitoKotlin) {
exclude module: "kotlin-stdlib"
}
testCompile(testing.kluent) {
exclude module: "kotlin-stdlib"
}
}