1
0
mirror of https://github.com/owncloud/android-library.git synced 2025-06-07 16:06:08 +00:00

Merge pull request #352 from owncloud/new_arch/moshi_parse_sharees

[New arch] Use moshi to parse sharees
This commit is contained in:
Abel García de Prada 2020-10-29 13:15:24 +01:00 committed by GitHub
commit af579a3fd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 326 additions and 105 deletions

View File

@ -14,6 +14,8 @@ dependencies {
exclude module: "kotlin-reflect" exclude module: "kotlin-reflect"
} }
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion"
testImplementation 'junit:junit:4.13'
} }
android { android {

View File

@ -21,8 +21,9 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package com.owncloud.android.lib.resources.response package com.owncloud.android.lib.resources
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
// Response retrieved by OCS Rest API, used to obtain capabilities, shares and user info among others. // Response retrieved by OCS Rest API, used to obtain capabilities, shares and user info among others.
@ -41,6 +42,11 @@ data class OCSResponse<T>(
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MetaData( data class MetaData(
val status: String, val status: String,
val statuscode: Int, @Json(name = "statuscode")
val message: String? val statusCode: Int,
val message: String?,
@Json(name = "itemsperpage")
val itemsPerPage: String?,
@Json(name = "totalitems")
val totalItems: String?
) )

View File

@ -1,5 +1,6 @@
/* ownCloud Android Library is available under MIT license /* ownCloud Android Library is available under MIT license
* *
* @author Christian Schabesberger
* @author masensio * @author masensio
* @author David A. Velasco * @author David A. Velasco
* @author David González Verdugo * @author David González Verdugo
@ -28,16 +29,21 @@
package com.owncloud.android.lib.resources.shares package com.owncloud.android.lib.resources.shares
import android.net.Uri
import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.http.HttpConstants import com.owncloud.android.lib.common.http.HttpConstants
import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod
import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperation
import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK
import org.json.JSONObject import com.owncloud.android.lib.resources.CommonOcsResponse
import com.owncloud.android.lib.resources.shares.responses.ShareeOcsResponse
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import timber.log.Timber import timber.log.Timber
import java.lang.reflect.Type
import java.net.URL import java.net.URL
import java.util.ArrayList
/** /**
* Created by masensio on 08/10/2015. * Created by masensio on 08/10/2015.
@ -65,6 +71,7 @@ import java.util.ArrayList
* Status codes: * Status codes:
* 100 - successful * 100 - successful
* *
* @author Christian Schabesberger
* @author masensio * @author masensio
* @author David A. Velasco * @author David A. Velasco
* @author David González Verdugo * @author David González Verdugo
@ -78,82 +85,68 @@ class GetRemoteShareesOperation
* @param perPage maximum number of results in a single page * @param perPage maximum number of results in a single page
*/ */
(private val searchString: String, private val page: Int, private val perPage: Int) : (private val searchString: String, private val page: Int, private val perPage: Int) :
RemoteOperation<ArrayList<JSONObject>>() { RemoteOperation<ShareeOcsResponse>() {
override fun run(client: OwnCloudClient): RemoteOperationResult<ArrayList<JSONObject>> { private fun buildRequestUri(baseUri: Uri) =
var result: RemoteOperationResult<ArrayList<JSONObject>> baseUri.buildUpon()
try {
val requestUri = client.baseUri
val uriBuilder = requestUri.buildUpon()
.appendEncodedPath(OCS_ROUTE) .appendEncodedPath(OCS_ROUTE)
.appendQueryParameter(PARAM_FORMAT, VALUE_FORMAT) .appendQueryParameter(PARAM_FORMAT, VALUE_FORMAT)
.appendQueryParameter(PARAM_ITEM_TYPE, VALUE_ITEM_TYPE) .appendQueryParameter(PARAM_ITEM_TYPE, VALUE_ITEM_TYPE)
.appendQueryParameter(PARAM_SEARCH, searchString) .appendQueryParameter(PARAM_SEARCH, searchString)
.appendQueryParameter(PARAM_PAGE, page.toString()) .appendQueryParameter(PARAM_PAGE, page.toString())
.appendQueryParameter(PARAM_PER_PAGE, perPage.toString()) .appendQueryParameter(PARAM_PER_PAGE, perPage.toString())
.build()
val getMethod = GetMethod(URL(uriBuilder.build().toString())) private fun parseResponse(response: String): ShareeOcsResponse? {
val moshi = Moshi.Builder().build()
getMethod.addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE) val type: Type = Types.newParameterizedType(CommonOcsResponse::class.java, ShareeOcsResponse::class.java)
val adapter: JsonAdapter<CommonOcsResponse<ShareeOcsResponse>> = moshi.adapter(type)
val status = client.executeHttpMethod(getMethod) return adapter.fromJson(response)!!.ocs.data
val response = getMethod.getResponseBodyAsString()
if (isSuccess(status)) {
Timber.d("Successful response: $response")
// Parse the response
val respJSON = JSONObject(response)
val respOCS = respJSON.getJSONObject(NODE_OCS)
val respData = respOCS.getJSONObject(NODE_DATA)
val respExact = respData.getJSONObject(NODE_EXACT)
val respExactUsers = respExact.getJSONArray(NODE_USERS)
val respExactGroups = respExact.getJSONArray(NODE_GROUPS)
val respExactRemotes = respExact.getJSONArray(NODE_REMOTES)
val respPartialUsers = respData.getJSONArray(NODE_USERS)
val respPartialGroups = respData.getJSONArray(NODE_GROUPS)
val respPartialRemotes = respData.getJSONArray(NODE_REMOTES)
val jsonResults = arrayOf(
respExactUsers,
respExactGroups,
respExactRemotes,
respPartialUsers,
respPartialGroups,
respPartialRemotes
)
val data = ArrayList<JSONObject>() // For result data
for (i in 0..5) {
for (j in 0 until jsonResults[i].length()) {
val jsonResult = jsonResults[i].getJSONObject(j)
data.add(jsonResult)
Timber.d("*** Added item: ${jsonResult.getString(PROPERTY_LABEL)}")
}
} }
result = RemoteOperationResult(OK) private fun onResultUnsuccessful(
result.data = data method: GetMethod,
response: String?,
Timber.d("*** Get Users or groups completed ") status: Int
): RemoteOperationResult<ShareeOcsResponse> {
} else {
result = RemoteOperationResult(getMethod)
Timber.e("Failed response while getting users/groups from the server ") Timber.e("Failed response while getting users/groups from the server ")
if (response != null) { if (response != null) {
Timber.e("*** status code: $status; response message: $response") Timber.e("*** status code: $status; response message: $response")
} else { } else {
Timber.e("*** status code: $status") Timber.e("*** status code: $status")
} }
} return RemoteOperationResult(method)
} catch (e: Exception) {
result = RemoteOperationResult(e)
Timber.e(e, "Exception while getting users/groups")
} }
private fun onRequestSuccessful(response: String?): RemoteOperationResult<ShareeOcsResponse> {
val result = RemoteOperationResult<ShareeOcsResponse>(OK)
Timber.d("Successful response: $response")
result.data = parseResponse(response!!)
Timber.d("*** Get Users or groups completed ")
return result return result
} }
override fun run(client: OwnCloudClient): RemoteOperationResult<ShareeOcsResponse> {
val requestUri = buildRequestUri(client.baseUri)
val getMethod = GetMethod(URL(requestUri.toString()))
getMethod.addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE)
return try {
val status = client.executeHttpMethod(getMethod)
val response = getMethod.getResponseBodyAsString()
if (!isSuccess(status)) {
onResultUnsuccessful(getMethod, response, status)
} else {
onRequestSuccessful(response)
}
} catch (e: Exception) {
Timber.e(e, "Exception while getting users/groups")
RemoteOperationResult(e)
}
}
private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK
companion object { companion object {
@ -171,18 +164,5 @@ class GetRemoteShareesOperation
// Arguments - constant values // Arguments - constant values
private const val VALUE_FORMAT = "json" private const val VALUE_FORMAT = "json"
private const val VALUE_ITEM_TYPE = "file" // to get the server search for users / groups private const val VALUE_ITEM_TYPE = "file" // to get the server search for users / groups
// JSON Node names
private const val NODE_OCS = "ocs"
private const val NODE_DATA = "data"
private const val NODE_EXACT = "exact"
private const val NODE_USERS = "users"
private const val NODE_GROUPS = "groups"
private const val NODE_REMOTES = "remotes"
const val NODE_VALUE = "value"
const val PROPERTY_LABEL = "label"
const val PROPERTY_SHARE_TYPE = "shareType"
const val PROPERTY_SHARE_WITH = "shareWith"
const val PROPERTY_SHARE_WITH_ADDITIONAL_INFO = "shareWithAdditionalInfo"
} }
} }

View File

@ -24,7 +24,6 @@
package com.owncloud.android.lib.resources.shares package com.owncloud.android.lib.resources.shares
import com.owncloud.android.lib.resources.files.FileUtils
import java.io.File import java.io.File
/** /**
@ -106,17 +105,6 @@ enum class ShareType constructor(val value: Int) {
FEDERATED(6); FEDERATED(6);
companion object { companion object {
fun fromValue(value: Int): ShareType? { fun fromValue(value: Int) = values().firstOrNull { it.value == value }
return when (value) {
-1 -> UNKNOWN
0 -> USER
1 -> GROUP
3 -> PUBLIC_LINK
4 -> EMAIL
5 -> CONTACT
6 -> FEDERATED
else -> null
}
}
} }
} }

View File

@ -0,0 +1,72 @@
/* ownCloud Android Library is available under MIT license
*
* Copyright (C) 2020 ownCloud GmbH.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.owncloud.android.lib.resources.shares.responses
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This was modeled according to the documentation:
* https://doc.owncloud.com/server/developer_manual/core/apis/ocs-recipient-api.html#get-shares-recipients
*/
@JsonClass(generateAdapter = true)
data class ShareeOcsResponse(
val exact: ExactSharees?,
val groups: List<ShareeItem>,
val remotes: List<ShareeItem>,
val users: List<ShareeItem>
) {
fun getFlatRepresentationWithoutExact() = ArrayList<ShareeItem>().apply {
addAll(users)
addAll(remotes)
addAll(groups)
}
}
@JsonClass(generateAdapter = true)
data class ExactSharees(
val groups: List<ShareeItem>,
val remotes: List<ShareeItem>,
val users: List<ShareeItem>
) {
fun getFlatRepresentation() = ArrayList<ShareeItem>().apply {
addAll(users)
addAll(remotes)
addAll(groups)
}
}
@JsonClass(generateAdapter = true)
data class ShareeItem(
val label: String,
val value: ShareeValue
)
@JsonClass(generateAdapter = true)
data class ShareeValue(
val shareType: Int,
val shareWith: String,
@Json(name = "shareWithAdditionalInfo")
val additionalInfo: String?
)

View File

@ -1,6 +1,7 @@
/** /**
* ownCloud Android client application * ownCloud Android client application
* *
* @author Christian Schabesberger
* @author David González Verdugo * @author David González Verdugo
* *
* Copyright (C) 2020 ownCloud GmbH. * Copyright (C) 2020 ownCloud GmbH.
@ -22,13 +23,12 @@ package com.owncloud.android.lib.resources.shares.services
import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.resources.Service import com.owncloud.android.lib.resources.Service
import org.json.JSONObject import com.owncloud.android.lib.resources.shares.responses.ShareeOcsResponse
import java.util.ArrayList
interface ShareeService : Service { interface ShareeService : Service {
fun getSharees( fun getSharees(
searchString: String, searchString: String,
page: Int, page: Int,
perPage: Int perPage: Int
): RemoteOperationResult<ArrayList<JSONObject>> ): RemoteOperationResult<ShareeOcsResponse>
} }

View File

@ -23,9 +23,8 @@ package com.owncloud.android.lib.resources.shares.services.implementation
import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.resources.shares.GetRemoteShareesOperation import com.owncloud.android.lib.resources.shares.GetRemoteShareesOperation
import com.owncloud.android.lib.resources.shares.responses.ShareeOcsResponse
import com.owncloud.android.lib.resources.shares.services.ShareeService import com.owncloud.android.lib.resources.shares.services.ShareeService
import org.json.JSONObject
import java.util.ArrayList
class OCShareeService(override val client: OwnCloudClient) : class OCShareeService(override val client: OwnCloudClient) :
ShareeService { ShareeService {
@ -33,7 +32,7 @@ class OCShareeService(override val client: OwnCloudClient) :
searchString: String, searchString: String,
page: Int, page: Int,
perPage: Int perPage: Int
): RemoteOperationResult<ArrayList<JSONObject>> = ): RemoteOperationResult<ShareeOcsResponse> =
GetRemoteShareesOperation( GetRemoteShareesOperation(
searchString, searchString,
page, page,

View File

@ -34,8 +34,8 @@ import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod
import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperation
import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK
import com.owncloud.android.lib.resources.response.CapabilityResponse import com.owncloud.android.lib.resources.status.responses.CapabilityResponse
import com.owncloud.android.lib.resources.response.CommonOcsResponse import com.owncloud.android.lib.resources.CommonOcsResponse
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.squareup.moshi.Types

View File

@ -22,7 +22,7 @@
* THE SOFTWARE. * THE SOFTWARE.
* *
*/ */
package com.owncloud.android.lib.resources.response package com.owncloud.android.lib.resources.status.responses
import com.owncloud.android.lib.resources.status.RemoteCapability import com.owncloud.android.lib.resources.status.RemoteCapability
import com.owncloud.android.lib.resources.status.RemoteCapability.CapabilityBooleanType import com.owncloud.android.lib.resources.status.RemoteCapability.CapabilityBooleanType

View File

@ -29,8 +29,8 @@ import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod
import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperation
import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
import com.owncloud.android.lib.resources.response.CommonOcsResponse import com.owncloud.android.lib.resources.CommonOcsResponse
import com.owncloud.android.lib.resources.response.UserInfoResponse import com.owncloud.android.lib.resources.users.responses.UserInfoResponse
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.squareup.moshi.Types

View File

@ -21,7 +21,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package com.owncloud.android.lib.resources.response package com.owncloud.android.lib.resources.users.responses
import com.owncloud.android.lib.resources.users.RemoteUserInfo import com.owncloud.android.lib.resources.users.RemoteUserInfo
import com.squareup.moshi.Json import com.squareup.moshi.Json

View File

@ -0,0 +1,93 @@
/* ownCloud Android Library is available under MIT license
* Copyright (C) 2020 ownCloud GmbH.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
package com.owncloud.android.lib.resources.shares.responses
import com.owncloud.android.lib.resources.CommonOcsResponse
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import org.junit.Assert.assertNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.io.File
import java.lang.reflect.Type
class ShareeResponseTest {
lateinit var adapter: JsonAdapter<CommonOcsResponse<ShareeOcsResponse>>
private fun loadResponses(fileName: String) =
adapter.fromJson(File(fileName).readText())
@Before
fun prepare() {
val moshi = Moshi.Builder().build()
val type: Type = Types.newParameterizedType(CommonOcsResponse::class.java, ShareeOcsResponse::class.java)
adapter = moshi.adapter(type)
}
@Test
fun `check structure - ok - contains meta`() {
val response = loadResponses(EXAMPLE_RESPONSE_JSON)!!
assertEquals("OK", response.ocs.meta.message!!)
assertEquals(200, response.ocs.meta.statusCode)
assertEquals("ok", response.ocs.meta.status)
assertTrue(response.ocs.meta.itemsPerPage?.isEmpty()!!)
assertTrue(response.ocs.meta.totalItems?.isEmpty()!!)
}
@Test
fun `example response - ok - correct sturcture`() {
val response = loadResponses(EXAMPLE_RESPONSE_JSON)!!
assertEquals(2, response.ocs.data.groups.size)
assertEquals(0, response.ocs.data.remotes.size)
assertEquals(2, response.ocs.data.users.size)
assertEquals(0, response.ocs.data.exact?.groups?.size)
assertEquals(0, response.ocs.data.exact?.remotes?.size)
assertEquals(1, response.ocs.data.exact?.users?.size)
assertEquals("user1@user1.com", response.ocs.data.users.get(0).value.additionalInfo)
assertNull(response.ocs.data.users[1].value.additionalInfo)
}
@Test
fun `check empty response - ok - parsing ok`() {
val response = loadResponses(EMPTY_RESPONSE_JSON)!!
assertTrue(response.ocs.data.exact?.groups?.isEmpty()!!)
assertTrue(response.ocs.data.exact?.remotes?.isEmpty()!!)
assertTrue(response.ocs.data.exact?.users?.isEmpty()!!)
assertTrue(response.ocs.data.groups.isEmpty())
assertTrue(response.ocs.data.remotes.isEmpty())
assertTrue(response.ocs.data.users.isEmpty())
}
companion object {
val RESOURCES_PATH =
"src/test/responses/com.owncloud.android.lib.resources.sharees.responses"
val EXAMPLE_RESPONSE_JSON = "$RESOURCES_PATH/example_sharee_response.json"
val EMPTY_RESPONSE_JSON = "$RESOURCES_PATH/empty_sharee_response.json"
}
}

View File

@ -0,0 +1,21 @@
{
"ocs": {
"meta": {
"status": "ok",
"statuscode": 100,
"message": "OK",
"totalitems": "",
"itemsperpage": ""
},
"data": {
"exact": {
"users": [],
"groups": [],
"remotes": []
},
"users": [],
"groups": [],
"remotes": []
}
}
}

View File

@ -0,0 +1,60 @@
{
"ocs": {
"data": {
"exact": {
"groups": [],
"remotes": [],
"users": [
{
"label": "admin",
"value": {
"shareType": 0,
"shareWith": "admin"
}
}
]
},
"groups": [
{
"label": "group1",
"value": {
"shareType": 1,
"shareWith": "group1"
}
},
{
"label": "group2",
"value": {
"shareType": 1,
"shareWith": "group2"
}
}
],
"remotes": [],
"users": [
{
"label": "user1",
"value": {
"shareType": 0,
"shareWith": "user1",
"shareWithAdditionalInfo": "user1@user1.com"
}
},
{
"label": "user2",
"value": {
"shareType": 0,
"shareWith": "user2"
}
}
]
},
"meta": {
"itemsperpage": "",
"message": "OK",
"status": "ok",
"statuscode": 200,
"totalitems": ""
}
}
}