From cfa90e0a8d2d4e88521d4c107e82ff19a9bd448c Mon Sep 17 00:00:00 2001 From: Yahor Berdnikau Date: Sat, 1 Apr 2017 18:40:14 +0200 Subject: [PATCH] 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 --- dependencies.gradle | 15 ++-- subsonic-api/build.gradle | 6 +- .../api/subsonic/SubsonicAPITest.kt | 31 +++++++- .../ultrasonic/api/subsonic/SubsonicAPI.kt | 16 ++-- .../api/subsonic/SubsonicAPIDefinition.kt | 5 +- .../api/subsonic/SubsonicAPIVersions.kt | 16 ++++ .../ultrasonic/api/subsonic/SubsonicError.kt | 16 ++++ .../ultrasonic/api/subsonic/models/License.kt | 5 ++ .../api/subsonic/models/SubsonicResponse.kt | 73 ------------------- .../api/subsonic/response/LicenseResponse.kt | 12 +++ .../api/subsonic/response/SubsonicResponse.kt | 39 ++++++++++ .../SubsonicAPIVersionsDeserializerTest.kt | 44 +++++++++++ .../api/subsonic/models/StatusTest.kt | 1 + ultrasonic/build.gradle | 14 +++- 14 files changed, 193 insertions(+), 100 deletions(-) create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/License.kt delete mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SubsonicResponse.kt create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/LicenseResponse.kt create mode 100644 subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SubsonicResponse.kt create mode 100644 subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersionsDeserializerTest.kt diff --git a/dependencies.gradle b/dependencies.gradle index 403b4fe7..78926565 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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 = [ diff --git a/subsonic-api/build.gradle b/subsonic-api/build.gradle index b9e606ee..eea7105f 100644 --- a/subsonic-api/build.gradle +++ b/subsonic-api/build.gradle @@ -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 diff --git a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPITest.kt b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPITest.kt index 80c9d8bd..d8019710 100644 --- a/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPITest.kt +++ b/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPITest.kt @@ -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) { + private fun assertResponseSuccessful(response: Response) { 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 + } } \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt index b22d6c19..9f22e81c 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPI.kt @@ -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) diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt index fa97e854..3bde6bd1 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt @@ -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 @GET("getLicense.view") - fun getLicense(): Call + fun getLicense(): Call } \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersions.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersions.kt index 940829fb..70596e7e 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersions.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersions.kt @@ -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() { + 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) + } + } } } \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicError.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicError.kt index 9e09682c..cce83f8a 100644 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicError.kt +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicError.kt @@ -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() { + 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 + } + } } } \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/License.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/License.kt new file mode 100644 index 00000000..5dffe77b --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/License.kt @@ -0,0 +1,5 @@ +package org.moire.ultrasonic.api.subsonic.models + +import java.util.* + +data class License(val valid: Boolean, val trialExpires: Calendar) \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SubsonicResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SubsonicResponse.kt deleted file mode 100644 index 32d8f3b1..00000000 --- a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/SubsonicResponse.kt +++ /dev/null @@ -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() { - 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 - } - } - } -} \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/LicenseResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/LicenseResponse.kt new file mode 100644 index 00000000..ea0186af --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/LicenseResponse.kt @@ -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) \ No newline at end of file diff --git a/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SubsonicResponse.kt b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SubsonicResponse.kt new file mode 100644 index 00000000..8f15947d --- /dev/null +++ b/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/response/SubsonicResponse.kt @@ -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() { + 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) + } + } + } + } +} \ No newline at end of file diff --git a/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersionsDeserializerTest.kt b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersionsDeserializerTest.kt new file mode 100644 index 00000000..c74135a7 --- /dev/null +++ b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIVersionsDeserializerTest.kt @@ -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() + private val context = mock() + + 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 + } +} \ No newline at end of file diff --git a/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/StatusTest.kt b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/StatusTest.kt index baa31842..b5cb4a84 100644 --- a/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/StatusTest.kt +++ b/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/StatusTest.kt @@ -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 diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 70938fab..506d0370 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -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" + } }