Как использовать ответ API Spring Data Rest с Retrofit 2, когда связь пуста? - PullRequest
0 голосов
/ 05 июля 2018

Я занимаюсь разработкой приложения для Android, а бэкэнд пишется с использованием Spring Rest Data.

Сценарий:

Допустим, я хочу перечислить любимые продукты пользователя. Мне нужно позвонить GET /v1/users/4/favoriteProducts с предоставлением токена доступа в заголовках. Если у пользователя есть любимые продукты, я получу ответ, который выглядит следующим образом:

{
    "links" : [ {
        "rel" : "self",
        "href" : "BASE_URL/v1/users/4/favoriteProducts",
        "hreflang" : null,
        "media" : null,
        "title" : null,
        "type" : null,
        "deprecation" : null
    } ],
    "content" : [ 
        {PRODUCT},
        {PRODUCT},
        {PRODUCT},
    ]
}

И я могу десериализовать это без проблем, я получаю HalList объект, который содержит три продукта. Мой сервисный вызов Retrofit выглядит следующим образом:

@GET("v1/users/{user}/favoriteProducts")
fun favoriteProducts(@Path("user") userId: Int): Call<HalList<Product>>

и мой HalList класс:

data class HalList<T>(
    @SerializedName("links")
    val links: List<HalLink>,

    @SerializedName("content")
    var content: List<T>
)

но если у меня нет любимых продуктов, я получаю что-то вроде этого:

{
    "links" : [ {
        "rel" : "self",
        "href" : "BASE_URL/v1/users/4/favoriteProducts",
        "hreflang" : null,
        "media" : null,
        "title" : null,
        "type" : null,
        "deprecation" : null
    } ],
   "content" : [ {
       "rel" : null,
       "collectionValue" : true,
       "relTargetType" : "com.example.entity.Product",
       "value" : [ ]
   } ]
}

И когда я анализирую это с помощью Retrofit, я получаю HalList объект с content, содержащий один экземпляр класса Product со всеми значениями, установленными в 0 или ноль или ложь, в зависимости от типа, и это создает проблему ..

Я написал это TypeAdapter для Gson, чтобы переопределить содержимое пустым списком, если оно пустое на основе json

class HalTypeAdapterFactory : TypeAdapterFactory {
    override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>?): TypeAdapter<T> {
        val delegate = gson?.getDelegateAdapter(this, type)!!

        if (!HalReflection.isResource(type?.rawType!!)) {
            return delegate
        }

        return HalTypeAdapter(gson, type, delegate)
    }
}

class HalTypeAdapter<T>(private val gson: Gson, private val type: TypeToken<T>, private val delegate: TypeAdapter<T>) : TypeAdapter<T>() {
    private val basicAdapter = gson.getAdapter(JsonElement::class.java)!!

    override fun write(out: JsonWriter?, value: T) {
        delegate.write(out, value)
    }

    override fun read(`in`: JsonReader?): T {
        val fullJson = basicAdapter.read(`in`)
        val deserialized = delegate.fromJsonTree(fullJson)

        if(type.rawType == HalList::class.java || type.rawType == HalPagedList::class.java) {
            if(fullJson.isJsonObject) {
                val content = fullJson.asJsonObject.getAsJsonArray("content")
                if(content[0].isJsonObject) {
                    val o = content[0].asJsonObject
                    if(o.has("collectionValue") && o.has("relTargetType")) {
                        val field = type.rawType.getDeclaredField("content")
                        field.isAccessible = true
                        field.set(deserialized, listOf<T>())
                        field.isAccessible = false
                    }
                }
            }
        }

        return deserialized
    }
}

Но я не уверен, что это правильное решение, и есть ли более элегантное решение? Также есть что-нибудь, что можно сделать на бэкенде, чтобы вернуть пустой список, например так:

{
    "links" : [ {
        "rel" : "self",
        "href" : "BASE_URL/v1/users/4/favoriteProducts",
        "hreflang" : null,
        "media" : null,
        "title" : null,
        "type" : null,
        "deprecation" : null
    } ],
   "content" : []
}

РЕДАКТИРОВАТЬ : окончательное решение

Мне не понравилась идея разрешить адаптеру делегата Gson анализировать ответ, а затем я проверяю строку json и использую отражение, чтобы изменить ее на пустой список. Я изменил свой подход, чтобы изменить ответ json перед десериализацией:

class HalTypeAdapterFactory : TypeAdapterFactory {
    override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>?): TypeAdapter<T> {
        val delegate = gson?.getDelegateAdapter(this, type)!!

        if (type?.rawType == HalList::class.java)
            return HalListTypeAdapter(gson, type, delegate)

        return delegate
    }
}

class HalListTypeAdapter<T>(
        private val gson: Gson,
        private val type: TypeToken<T>?,
        private val delegate: TypeAdapter<T>)
    : TypeAdapter<T>() {

    private val basicAdapter = gson.getAdapter(JsonElement::class.java)

    override fun write(out: JsonWriter?, value: T) {
        delegate.write(out, value)
    }

    override fun read(`in`: JsonReader?): T {
        val json = basicAdapter.read(`in`)

        if (json.isJsonObject) {
            val resource = json.asJsonObject
            val content = resource.getAsJsonArray("content")
            if (isEmptyCollection(content)) {
                resource.remove("content")
                resource.add("content", JsonArray())
            }
        }

        return delegate.fromJsonTree(json)
    }

    private fun isEmptyCollection(content: JsonArray): Boolean {
        if (content.size() == 1 && content[0].isJsonObject) {
            val first = content[0].asJsonObject
            return first.has("collectionValue") && first.has("relTargetType")
        }

        return false
    }
}

data class HalList<T>(
        @SerializedName("links")
        val links: List<HalReference>,

        @SerializedName("page")
        val page: HalListPageMeta?,

        @SerializedName("content")
        val content: List<T>
)
...