Skip to content

Commit

Permalink
Implement Binary Upload (Fixes #2126) - WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
allentiak committed Jan 28, 2025
1 parent 5893170 commit d19d3d3
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 7 deletions.
14 changes: 12 additions & 2 deletions modules/rest-api/src/blaze/rest_api/routes.clj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
{:name :auth-guard
:wrap auth-guard/wrap-auth-guard})

(def ^:private wrap-binary-resource
{:name :resource
:wrap resource/wrap-binary-resource})

(def ^:private wrap-resource
{:name :resource
:wrap resource/wrap-resource})
Expand Down Expand Up @@ -103,6 +107,8 @@
{:fhir.resource/type name}
[""
(cond-> {:name (keyword name "type")}
(= name "Binary")
(assoc :response-type :binary)
(contains? interactions :search-type)
(assoc :get {:interaction "search-type"
:middleware [[wrap-db node db-sync-timeout]
Expand All @@ -111,7 +117,9 @@
:blaze.rest-api.interaction/handler)})
(contains? interactions :create)
(assoc :post {:interaction "create"
:middleware [wrap-resource]
:middleware (if (:response-type :binary)
[wrap-binary-resource]
[wrap-resource])
:handler (-> interactions :create
:blaze.rest-api.interaction/handler)})
(contains? interactions :conditional-delete-type)
Expand Down Expand Up @@ -179,7 +187,9 @@
:blaze.rest-api.interaction/handler)})
(contains? interactions :update)
(assoc :put {:interaction "update"
:middleware [wrap-resource]
:middleware (if (:response-type :binary)
[wrap-binary-resource]
[wrap-resource])
:handler (-> interactions :update
:blaze.rest-api.interaction/handler)})
(contains? interactions :delete)
Expand Down
51 changes: 51 additions & 0 deletions modules/rest-util/src/blaze/middleware/fhir/resource.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
[blaze.anomaly :as ba :refer [if-ok when-ok]]
[blaze.async.comp :as ac]
[blaze.fhir.spec :as fhir-spec]
[blaze.fhir.spec.type :as type]
[clojure.data.xml.jvm.parse :as xml-jvm]
[clojure.data.xml.tree :as xml-tree]
[clojure.java.io :as io]
Expand All @@ -17,6 +18,7 @@
(:import
[com.ctc.wstx.api WstxInputProperties]
[java.io Reader]
[java.util Base64 Base64$Encoder]
[javax.xml.stream XMLInputFactory]))

(set! *warn-on-reflection* true)
Expand Down Expand Up @@ -95,6 +97,29 @@
(assoc request :body resource))
(ba/incorrect "Missing HTTP body.")))

(def ^:private ^Base64$Encoder b64-encoder
(.withoutPadding (Base64/getUrlEncoder)))

(defn- resource-request-binary** [data]
(with-open [_ (prom/timer parse-duration-seconds "binary")]
(.encodeToString b64-encoder data)))

(defn- binary-content-type [body]
(or (-> body :contentType type/value)
"application/octet-stream"))

(defn- resource-request-binary* [{:keys [data contentType] :as body}]
(when data
(assoc body
:data (resource-request-binary** data)
:contentType (binary-content-type contentType))))

(defn- resource-request-binary [{:keys [body] :as request}]
(if body
(when-ok [resource (resource-request-binary* body)]
(assoc request :body resource))
(ba/incorrect "Missing HTTP body.")))

(defn- unsupported-media-type-msg [media-type]
(format "Unsupported media type `%s` expect one of `application/fhir+json` or `application/fhir+xml`."
media-type))
Expand Down Expand Up @@ -125,3 +150,29 @@
(if-ok [request (resource-request request)]
(handler request)
ac/completed-future)))

(defn- binary-resource-request [request]
(if-let [content-type (request/content-type request)]
(cond
(json-request? content-type) (resource-request-json request)
(xml-request? content-type) (resource-request-xml request)
:else
(resource-request-binary request))
(if (str/blank? (slurp (:body request)))
(assoc request :body nil)
(ba/incorrect "Content-Type header expected, but is missing."))))

(defn wrap-binary-resource
"Middleware to parse a binary resource from the body according the content-type
header.
Updates the :body key in the request map on successful parsing and conforming
the resource to the internal format.
Returns an OperationOutcome in the internal format, skipping the handler, with
an appropriate error on parsing and conforming errors."
[handler]
(fn [request]
(if-ok [request (binary-resource-request request)]
(handler request)
ac/completed-future)))
53 changes: 48 additions & 5 deletions modules/rest-util/test/blaze/middleware/fhir/resource_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
(:require
[blaze.async.comp :as ac]
[blaze.fhir.spec :as fhir-spec]
[blaze.fhir.spec.type :as type]
[blaze.fhir.test-util]
[blaze.handler.util :as handler-util]
[blaze.middleware.fhir.resource :refer [wrap-resource]]
[blaze.middleware.fhir.resource :refer [wrap-binary-resource wrap-resource]]
[blaze.test-util :as tu :refer [satisfies-prop]]
[clojure.data.xml :as xml]
[clojure.java.io :as io]
[clojure.spec.test.alpha :as st]
[clojure.string :as str]
[clojure.test :as test :refer [deftest is testing]]
Expand All @@ -23,18 +26,31 @@

(test/use-fixtures :each tu/fixture)

(defn wrap-error [handler]
(defn- wrap-error [handler]
(fn [request]
(-> (handler request)
(ac/exceptionally handler-util/error-response))))

(def resource-handler
"A handler which just returns the :body from the request."
(defn- parse-json [body]
(fhir-spec/conform-json (fhir-spec/parse-json body)))

(defn- parse-xml [body]
(with-open [reader (io/reader body)]
(fhir-spec/conform-xml (xml/parse reader))))

(def ^:private resource-handler
"A handler which just returns the `:body` from a non-binary resource request."
(-> (comp ac/completed-future :body)
wrap-resource
wrap-error))

(defn input-stream
(def ^:private binary-resource-handler
"A handler which just returns the `:body` from a binary resource request."
(-> (comp ac/completed-future :body)
wrap-binary-resource
wrap-error))

(defn- input-stream
([^String s]
(ByteArrayInputStream. (.getBytes s StandardCharsets/UTF_8)))
([^String s closed?]
Expand Down Expand Up @@ -193,6 +209,33 @@
:body (input-stream (str "<Binary xmlns=\"http://hl7.org/fhir\"><data value=\"" (apply str (repeat (* 8 1024 1024) \a)) "\"/></Binary>"))})
fhir-spec/fhir-type := :fhir/Binary)))

(deftest binary-test
(testing "returning the FHIR resource (both as JSON and as XML)"
(let [binary-data "MTA1NjE0Cg=="]
(doseq [[content-type body-parser resource-string-representation]
[["application/fhir+json;charset=utf-8" parse-json (str "{\"data\" : \"" binary-data "\", \"resourceType\" : \"Binary\"}")]
["application/fhir+xml;charset=utf-8" parse-xml (str "<Binary xmlns=\"http://hl7.org/fhir\"><data value=\"" binary-data "\"/></Binary>")]]]
(let [closed? (atom false)]
(given @(binary-resource-handler
{:headers {"content-type" content-type}
:body (input-stream resource-string-representation closed?)})
:status := 200
identity := "this is what I get"
fhir-spec/fhir-type := :fhir/Binary
[:headers "Content-Type"] := content-type
[:body body-parser] := {:fhir/type :fhir/Binary
:contentType (type/code content-type)
:data #fhir/base64Binary"MTA1NjE0Cg=="}))))))

(comment
(str "{\"data\" : \"" "MTA1NjE0Cg==" "\", \"resourceType\" : \"Binary\"}")
;; => "{\"data\" : \"MTA1NjE0Cg==\", \"resourceType\" : \"Binary\"}"

(str "<Binary xmlns=\"http://hl7.org/fhir\"><data value=\"" "MTA1NjE0Cg==" "\"/></Binary>")
;; => "<Binary xmlns=\"http://hl7.org/fhir\"><data value=\"MTA1NjE0Cg==\"/></Binary>"

:end)

(def ^:private whitespace
(gen/fmap str/join (gen/vector (gen/elements [" " "\n" "\r" "\t"]))))

Expand Down

0 comments on commit d19d3d3

Please sign in to comment.