Skip to content

Commit

Permalink
Support Uploading Individual requests - Not exposed yet (google#2121)
Browse files Browse the repository at this point in the history
* Create IndividualRequestGenerator

* start adding tests

* constructor changes

* small fix

* address review comments

* inline func
  • Loading branch information
omarismail94 authored Aug 22, 2023
1 parent b7b8b98 commit f71dab6
Show file tree
Hide file tree
Showing 10 changed files with 705 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class GzipUploadInterceptorBenchmark {

@GET suspend fun get(@Url path: String): Resource

@POST(".") suspend fun post(@Body bundle: Bundle): Resource
@POST(".") suspend fun post(@Body bundle: Resource): Resource

class Builder(
private val baseUrl: String,
Expand Down
7 changes: 5 additions & 2 deletions engine/src/main/java/com/google/android/fhir/sync/Request.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.google.android.fhir.sync
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.codesystems.HttpVerb

/**
* Structure represents a request that can be made to download resources from the FHIR server. The
Expand Down Expand Up @@ -90,7 +91,7 @@ internal constructor(val bundle: Bundle, override val headers: Map<String, Strin
* FHIR server.
*/
sealed class UploadRequest(
val url: String,
open val url: String,
open val headers: Map<String, String> = emptyMap(),
open val resource: Resource,
open val localChangeToken: LocalChangeToken,
Expand All @@ -108,7 +109,9 @@ data class BundleUploadRequest(

/** A [url] based FHIR request to upload resources to the server. */
data class UrlUploadRequest(
val httpVerb: HttpVerb,
override val url: String,
override val resource: Resource,
override val localChangeToken: LocalChangeToken,
override val headers: Map<String, String> = emptyMap()
) : UploadRequest(resource.resourceType.name, headers, resource, localChangeToken)
) : UploadRequest(url, headers, resource, localChangeToken)
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,21 @@ package com.google.android.fhir.sync.remote
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.fge.jsonpatch.JsonPatch
import com.google.android.fhir.ContentTypes
import com.google.android.fhir.MediaTypes
import java.lang.reflect.Type
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import org.hl7.fhir.r4.model.Resource
import retrofit2.Converter
import retrofit2.Retrofit

class FhirConverterFactory private constructor(val fhirContext: FhirContext) : Converter.Factory() {
class FhirConverterFactory private constructor(private val fhirContext: FhirContext) :
Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation>,
Expand All @@ -40,7 +45,12 @@ class FhirConverterFactory private constructor(val fhirContext: FhirContext) : C
parameterAnnotations: Array<out Annotation>,
methodAnnotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<*, RequestBody> = FhirRequestBodyConverter(fhirContext.newJsonParser())
): Converter<*, RequestBody> =
when (type) {
Resource::class.java -> FhirRequestBodyConverter(fhirContext.newJsonParser())
JsonPatch::class.java -> JsonPatchBodyConverter()
else -> error("Request body converter for type ${type.javaClass} does not exist")
}

companion object {
fun create() = FhirConverterFactory(FhirContext.forCached(FhirVersionEnum.R4))
Expand All @@ -62,3 +72,13 @@ private class FhirRequestBodyConverter(private val parser: IParser) :
return parser.encodeResourceToString(value).toRequestBody(MediaTypes.MEDIA_TYPE_FHIR_JSON)
}
}

/** Retrofit converter that allows us to convert a JSON Patch in the requests. */
private class JsonPatchBodyConverter : Converter<JsonPatch, RequestBody> {
override fun convert(value: JsonPatch): RequestBody {
val mapper = ObjectMapper()
return mapper
.writeValueAsString(value)
.toRequestBody(ContentTypes.APPLICATION_JSON_PATCH.toMediaType())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@

package com.google.android.fhir.sync.remote

import com.fasterxml.jackson.databind.ObjectMapper
import com.github.fge.jsonpatch.JsonPatch
import com.google.android.fhir.sync.BundleDownloadRequest
import com.google.android.fhir.sync.BundleUploadRequest
import com.google.android.fhir.sync.DataSource
import com.google.android.fhir.sync.DownloadRequest
import com.google.android.fhir.sync.UploadRequest
import com.google.android.fhir.sync.UrlDownloadRequest
import com.google.android.fhir.sync.UrlUploadRequest
import org.hl7.fhir.r4.model.Binary
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.codesystems.HttpVerb

/**
* Implementation of [DataSource] to sync data with the FHIR server using HTTP method calls.
Expand All @@ -37,5 +43,27 @@ internal class FhirHttpDataSource(private val fhirHttpService: FhirHttpService)
}

override suspend fun upload(request: UploadRequest): Resource =
fhirHttpService.post(request.url, request.resource, request.headers)
when (request) {
is BundleUploadRequest -> fhirHttpService.post(request.url, request.resource, request.headers)
is UrlUploadRequest -> uploadIndividualRequest(request)
}

private suspend fun uploadIndividualRequest(request: UrlUploadRequest): Resource =
when (request.httpVerb) {
HttpVerb.POST -> fhirHttpService.post(request.url, request.resource, request.headers)
HttpVerb.PUT -> fhirHttpService.put(request.url, request.resource, request.headers)
HttpVerb.PATCH ->
fhirHttpService.patch(request.url, request.resource.toJsonPatch(), request.headers)
HttpVerb.DELETE -> fhirHttpService.delete(request.url, request.headers)
else -> error("The method, ${request.httpVerb}, is not supported for upload")
}
}

private fun Resource.toJsonPatch(): JsonPatch =
when (this) {
is Binary -> {
val objectMapper = ObjectMapper()
objectMapper.readValue(String(this.data), JsonPatch::class.java)
}
else -> error("This resource cannot have the PATCH operation be applied to it")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.android.fhir.sync.remote

import com.github.fge.jsonpatch.JsonPatch
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.OperationOutcome
import org.hl7.fhir.r4.model.Resource
Expand All @@ -35,4 +36,22 @@ internal interface FhirHttpService {
* @return The server may return [Bundle] or [OperationOutcome] based on the request processing.
*/
suspend fun post(path: String, resource: Resource, headers: Map<String, String>): Resource

/**
* Makes a HTTP-PUT method request to the server with a [Resource] as request-body.
* @return The server may return [Bundle] or [OperationOutcome] based on the request processing.
*/
suspend fun put(path: String, resource: Resource, headers: Map<String, String>): Resource

/**
* Makes a HTTP-PATCH method request to the server with a [JsonPatch] as request-body.
* @return The server may return [Bundle] or [OperationOutcome] based on the request processing.
*/
suspend fun patch(path: String, patchDocument: JsonPatch, headers: Map<String, String>): Resource

/**
* Makes a HTTP-DELETE method request to the server.
* @return The server may return [Bundle] or [OperationOutcome] based on the request processing.
*/
suspend fun delete(path: String, headers: Map<String, String>): Resource
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.android.fhir.sync.remote

import com.github.fge.jsonpatch.JsonPatch
import com.google.android.fhir.NetworkConfiguration
import com.google.android.fhir.sync.HttpAuthenticator
import java.util.concurrent.TimeUnit
Expand All @@ -25,9 +26,12 @@ import okhttp3.logging.HttpLoggingInterceptor
import org.hl7.fhir.r4.model.Resource
import retrofit2.Retrofit
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.HeaderMap
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Url

/** Retrofit service to make http requests to the FHIR server. */
Expand All @@ -43,6 +47,23 @@ internal interface RetrofitHttpService : FhirHttpService {
@HeaderMap headers: Map<String, String>
): Resource

@PUT
override suspend fun put(
@Url path: String,
@Body resource: Resource,
@HeaderMap headers: Map<String, String>
): Resource

@PATCH
override suspend fun patch(
@Url path: String,
@Body patchDocument: JsonPatch,
@HeaderMap headers: Map<String, String>,
): Resource

@DELETE
override suspend fun delete(@Url path: String, @HeaderMap headers: Map<String, String>): Resource

class Builder(
private val baseUrl: String,
private val networkConfiguration: NetworkConfiguration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.sync.upload

import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import com.google.android.fhir.ContentTypes
import com.google.android.fhir.LocalChange
import com.google.android.fhir.LocalChange.Type
import com.google.android.fhir.db.impl.dao.LocalChangeToken
import com.google.android.fhir.sync.UrlUploadRequest
import org.hl7.fhir.r4.model.Binary
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.codesystems.HttpVerb

/** Generates list of [UrlUploadRequest]s with for each [LocalChange] given. */
class IndividualRequestGenerator(
private val getIndividualRequestForLocalChangeType:
(type: Type, localChange: LocalChange) -> UrlUploadRequest
) : UploadRequestGenerator {

override fun generateUploadRequests(localChanges: List<LocalChange>): List<UrlUploadRequest> =
localChanges.map { getIndividualRequestForLocalChangeType(it.type, it) }

companion object Factory {

private val parser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser()

private val createMapping =
mapOf(
HttpVerb.POST to this::postForCreateResource,
HttpVerb.PUT to this::putForCreateResource,
)

private val updateMapping =
mapOf(
HttpVerb.PATCH to this::patchForUpdateResource,
)

fun getDefault() = getGenerator(HttpVerb.PUT, HttpVerb.PATCH)

/**
* Returns a [IndividualRequestGenerator] based on the provided [HttpVerb]s for creating and
* updating resources. The function may throw an [IllegalArgumentException] if the provided
* [HttpVerb]s are not supported.
*/
fun getGenerator(
httpVerbToUseForCreate: HttpVerb,
httpVerbToUseForUpdate: HttpVerb
): IndividualRequestGenerator {

val createFunction =
createMapping[httpVerbToUseForCreate]
?: throw IllegalArgumentException(
"Creation using $httpVerbToUseForCreate is not supported."
)

val updateFunction =
updateMapping[httpVerbToUseForUpdate]
?: throw IllegalArgumentException(
"Update using $httpVerbToUseForUpdate is not supported."
)

return IndividualRequestGenerator { type, localChange ->
when (type) {
Type.INSERT -> createFunction(localChange)
Type.UPDATE -> updateFunction(localChange)
Type.DELETE -> deleteFunction(localChange)
Type.NO_OP -> error("Cannot create a request from a NO_OP type")
}
}
}

private fun deleteFunction(localChange: LocalChange) =
UrlUploadRequest(
httpVerb = HttpVerb.DELETE,
url = "${localChange.resourceType}/${localChange.resourceId}",
resource = parser.parseResource(localChange.payload) as Resource,
localChangeToken = LocalChangeToken(localChange.token.ids),
)

private fun postForCreateResource(localChange: LocalChange) =
UrlUploadRequest(
httpVerb = HttpVerb.POST,
url = localChange.resourceType,
resource = parser.parseResource(localChange.payload) as Resource,
localChangeToken = LocalChangeToken(localChange.token.ids),
)

private fun putForCreateResource(localChange: LocalChange) =
UrlUploadRequest(
httpVerb = HttpVerb.PUT,
url = "${localChange.resourceType}/${localChange.resourceId}",
resource = parser.parseResource(localChange.payload) as Resource,
localChangeToken = LocalChangeToken(localChange.token.ids),
)

private fun patchForUpdateResource(localChange: LocalChange) =
UrlUploadRequest(
httpVerb = HttpVerb.PATCH,
url = "${localChange.resourceType}/${localChange.resourceId}",
resource = Binary().apply { data = localChange.payload.toByteArray() },
localChangeToken = LocalChangeToken(localChange.token.ids),
headers = mapOf("Content-Type" to ContentTypes.APPLICATION_JSON_PATCH)
)
}
}
Loading

0 comments on commit f71dab6

Please sign in to comment.