diff --git a/modules/rest-api/src/blaze/rest_api/routes.clj b/modules/rest-api/src/blaze/rest_api/routes.clj
index 61240f7b8..0d1a69fd5 100644
--- a/modules/rest-api/src/blaze/rest_api/routes.clj
+++ b/modules/rest-api/src/blaze/rest_api/routes.clj
@@ -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})
@@ -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]
@@ -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)
@@ -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)
diff --git a/modules/rest-util/src/blaze/middleware/fhir/resource.clj b/modules/rest-util/src/blaze/middleware/fhir/resource.clj
index 9e391d770..7f27bcb44 100644
--- a/modules/rest-util/src/blaze/middleware/fhir/resource.clj
+++ b/modules/rest-util/src/blaze/middleware/fhir/resource.clj
@@ -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]
@@ -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)
@@ -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))
@@ -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)))
diff --git a/modules/rest-util/test/blaze/middleware/fhir/resource_test.clj b/modules/rest-util/test/blaze/middleware/fhir/resource_test.clj
index d17f6df1f..cd2ffe001 100644
--- a/modules/rest-util/test/blaze/middleware/fhir/resource_test.clj
+++ b/modules/rest-util/test/blaze/middleware/fhir/resource_test.clj
@@ -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]]
@@ -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?]
@@ -193,6 +209,33 @@
:body (input-stream (str ""))})
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 "")]]]
+ (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 "")
+ ;; => ""
+
+ :end)
+
(def ^:private whitespace
(gen/fmap str/join (gen/vector (gen/elements [" " "\n" "\r" "\t"]))))