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"]))))