2014-07-04 09:04:45 -04:00
|
|
|
(ns clj-hl7-fhir.core
|
2014-07-04 09:40:51 -04:00
|
|
|
(:import (java.util Date)
|
|
|
|
(clojure.lang ExceptionInfo))
|
2014-07-14 10:19:02 -04:00
|
|
|
(:require [clojure.string :as str]
|
|
|
|
[cheshire.core :as json])
|
2014-07-04 09:04:45 -04:00
|
|
|
(:use [camel-snake-kebab]
|
|
|
|
[clj-hl7-fhir.util]))
|
|
|
|
|
|
|
|
(defn- ->fhir-resource-name [x]
|
|
|
|
(name (->CamelCase x)))
|
|
|
|
|
2014-07-08 14:24:55 -04:00
|
|
|
(defn- fhir-request [type base-url resource-url & {:keys [params body]}]
|
|
|
|
(let [query (map->query-string params)
|
|
|
|
url (build-url base-url resource-url query)]
|
2014-07-14 10:19:02 -04:00
|
|
|
(try
|
2014-07-14 11:26:25 -04:00
|
|
|
(let [response (case type
|
|
|
|
:get (http-get-json url)
|
|
|
|
:post (http-post-json url body)
|
|
|
|
:put (http-put-json url body)
|
|
|
|
:delete (http-delete-json url body))]
|
2014-07-18 13:30:09 -04:00
|
|
|
(-> (if-let [location (get-in response [:headers "Location"])]
|
|
|
|
(http-get-json location)
|
2014-07-14 11:26:25 -04:00
|
|
|
response)
|
|
|
|
:body
|
|
|
|
(json/parse-string true)))
|
2014-07-14 10:19:02 -04:00
|
|
|
(catch ExceptionInfo ex
|
2014-07-14 11:03:51 -04:00
|
|
|
(let [{:keys [status body headers]} (:object (ex-data ex))
|
|
|
|
fhir-resource-response? (.contains (get headers "Content-Type") "application/json+fhir")]
|
|
|
|
(throw (ex-info (str "FHIR request failed: HTTP " status)
|
|
|
|
{:status status
|
|
|
|
:fhir-resource? fhir-resource-response?
|
|
|
|
:response
|
|
|
|
(if fhir-resource-response?
|
|
|
|
(json/parse-string body true)
|
|
|
|
body)})))))))
|
2014-07-04 09:04:45 -04:00
|
|
|
|
2014-07-04 10:54:19 -04:00
|
|
|
(defn- ->search-param-name [parameter & [modifier]]
|
2014-07-04 09:04:45 -04:00
|
|
|
(keyword
|
|
|
|
(str
|
2014-07-04 11:15:42 -04:00
|
|
|
(if (vector? parameter)
|
|
|
|
(->> parameter
|
|
|
|
(map name)
|
|
|
|
(str/join ".")
|
|
|
|
)
|
|
|
|
(name parameter))
|
2014-07-04 09:04:45 -04:00
|
|
|
(if modifier
|
|
|
|
(str ":" (name modifier))))))
|
|
|
|
|
2014-07-08 08:22:21 -04:00
|
|
|
(defn- ->search-param-descriptor [parameter value operator {:keys [modifier]}]
|
2014-07-04 10:54:19 -04:00
|
|
|
{:name (->search-param-name parameter modifier)
|
|
|
|
:operator operator
|
|
|
|
:value value})
|
|
|
|
|
2014-07-08 08:22:21 -04:00
|
|
|
(defmacro ^:private single-search-op [name operator]
|
2014-07-04 10:54:19 -04:00
|
|
|
`(defn ~name [parameter# value# & options#]
|
|
|
|
[(->search-param-descriptor parameter# value# ~operator (apply hash-map options#))]))
|
2014-07-04 09:04:45 -04:00
|
|
|
|
2014-07-08 08:22:21 -04:00
|
|
|
(defmacro ^:private double-search-op [name operator1 operator2]
|
2014-07-04 10:54:19 -04:00
|
|
|
`(defn ~name [parameter# value1# value2# & options#]
|
|
|
|
[(->search-param-descriptor parameter# value1# ~operator1 (apply hash-map options#))
|
|
|
|
(->search-param-descriptor parameter# value2# ~operator2 (apply hash-map options#))]))
|
2014-07-04 09:04:45 -04:00
|
|
|
|
2014-07-04 10:54:19 -04:00
|
|
|
(defn- escape-parameter [value]
|
|
|
|
(-> value
|
|
|
|
(.replace "\\" "\\\\")
|
|
|
|
(.replace "$" "\\$")
|
|
|
|
(.replace "," "\\,")
|
|
|
|
(.replace "|" "\\|")))
|
|
|
|
|
|
|
|
(defn- format-search-value [value]
|
|
|
|
(cond
|
|
|
|
(sequential? value)
|
|
|
|
(->> value
|
|
|
|
(map format-search-value)
|
|
|
|
(str/join ","))
|
|
|
|
|
|
|
|
(map? value)
|
|
|
|
(str (:namespace value) "|" (format-search-value (:value value)))
|
|
|
|
|
|
|
|
(instance? Date value)
|
2014-07-08 10:07:36 -04:00
|
|
|
(->timestamp value)
|
2014-07-04 10:54:19 -04:00
|
|
|
|
|
|
|
:else
|
|
|
|
(-> value str escape-parameter)))
|
|
|
|
|
2014-07-04 18:55:00 -04:00
|
|
|
(defn- search-params->query-map [params]
|
2014-07-04 10:54:19 -04:00
|
|
|
(->> params
|
|
|
|
(apply concat)
|
|
|
|
(map
|
|
|
|
(fn [{:keys [name operator value]}]
|
|
|
|
[name
|
|
|
|
(str
|
|
|
|
(if-not (= "=" operator) operator)
|
|
|
|
(format-search-value value))]))
|
2014-07-04 18:55:00 -04:00
|
|
|
(reduce
|
|
|
|
(fn [m [name value]]
|
|
|
|
(if (contains? m name)
|
|
|
|
(update-in m [name] #(conj (if (vector? %) % [%]) value))
|
|
|
|
(assoc m name value)))
|
|
|
|
{})))
|
2014-07-04 10:54:19 -04:00
|
|
|
|
2014-07-04 13:40:27 -04:00
|
|
|
(defn- get-bundle-next-page-url [bundle]
|
|
|
|
(->> (:link bundle)
|
|
|
|
(filter #(= "next" (:rel %)))
|
|
|
|
(first)
|
|
|
|
:href))
|
|
|
|
|
2014-07-08 08:22:21 -04:00
|
|
|
(single-search-op eq "=")
|
|
|
|
(single-search-op lt "<")
|
|
|
|
(single-search-op lte "<=")
|
|
|
|
(single-search-op gt ">")
|
|
|
|
(single-search-op gte ">=")
|
|
|
|
(double-search-op between ">" "<")
|
|
|
|
|
|
|
|
(defn namespaced
|
|
|
|
([value]
|
|
|
|
(namespaced nil value))
|
|
|
|
([namespace value]
|
|
|
|
{:namespace namespace
|
|
|
|
:value value}))
|
|
|
|
|
2014-07-04 12:34:04 -04:00
|
|
|
(defn collect-resources
|
2014-07-04 13:56:43 -04:00
|
|
|
"returns a sequence containing all of the resources contained in the given bundle
|
|
|
|
|
|
|
|
reference:
|
|
|
|
bundles: http://hl7.org/implement/standards/fhir/extras.html#bundle"
|
2014-07-04 12:34:04 -04:00
|
|
|
[bundle]
|
|
|
|
(->> bundle
|
|
|
|
:entry
|
|
|
|
(map :content)))
|
|
|
|
|
2014-07-04 13:40:27 -04:00
|
|
|
(defn fetch-next-page
|
|
|
|
"for resources that are returned over more then one page, this will fetch the
|
|
|
|
next page of resources as indicated by the link information contained in the
|
|
|
|
passed bundle. the return value is another bundle that can be passed again
|
|
|
|
to this function to get subsequent pages. if this function is passed the
|
2014-07-04 13:56:43 -04:00
|
|
|
bundle for the last page of resources, nil is returned
|
|
|
|
|
|
|
|
reference:
|
|
|
|
bundles: http://hl7.org/implement/standards/fhir/extras.html#bundle
|
|
|
|
paging: http://hl7.org/implement/standards/fhir/http.html#paging"
|
2014-07-04 13:40:27 -04:00
|
|
|
[bundle]
|
|
|
|
(if-let [next-url (get-bundle-next-page-url bundle)]
|
|
|
|
(http-get-json next-url)))
|
|
|
|
|
2014-07-08 11:08:10 -04:00
|
|
|
(defn- concat-bundle-entries [bundle other-bundle]
|
|
|
|
(if (nil? bundle)
|
|
|
|
other-bundle
|
|
|
|
(update-in
|
|
|
|
bundle [:entry]
|
|
|
|
(fn [existing-entries]
|
|
|
|
(->> (:entry other-bundle)
|
|
|
|
(concat existing-entries)
|
|
|
|
(vec))))))
|
|
|
|
|
|
|
|
(defn- strip-bundle-page-links [bundle]
|
|
|
|
(if bundle
|
|
|
|
(assoc bundle
|
|
|
|
:link
|
|
|
|
(->> (:link bundle)
|
|
|
|
(remove
|
|
|
|
(fn [{:keys [rel]}]
|
|
|
|
(or (= rel "first")
|
|
|
|
(= rel "last")
|
|
|
|
(= rel "next")
|
|
|
|
(= rel "previous"))))
|
|
|
|
(vec)))))
|
|
|
|
|
2014-07-04 13:40:27 -04:00
|
|
|
(defn fetch-all
|
|
|
|
"for resources that are returned over more then one page, this will automatically
|
2014-07-08 11:08:10 -04:00
|
|
|
fetch all pages of resources and them into a single bundle that contains all of
|
|
|
|
the resources.
|
2014-07-04 13:56:43 -04:00
|
|
|
|
|
|
|
reference:
|
|
|
|
bundles: http://hl7.org/implement/standards/fhir/extras.html#bundle
|
|
|
|
paging: http://hl7.org/implement/standards/fhir/http.html#paging"
|
2014-07-04 13:40:27 -04:00
|
|
|
[bundle]
|
2014-07-08 11:08:10 -04:00
|
|
|
(loop [current-page bundle
|
|
|
|
working-bundle nil]
|
|
|
|
(let [merged (concat-bundle-entries working-bundle current-page)
|
|
|
|
next-page (fetch-next-page current-page)]
|
2014-07-04 13:40:27 -04:00
|
|
|
(if next-page
|
2014-07-08 11:08:10 -04:00
|
|
|
(recur next-page merged)
|
|
|
|
(strip-bundle-page-links merged)))))
|
2014-07-04 13:40:27 -04:00
|
|
|
|
2014-07-04 09:04:45 -04:00
|
|
|
(defn get-resource
|
2014-07-08 13:52:54 -04:00
|
|
|
"gets a single resource from a FHIR server. the raw resource itself is returned (that is,
|
|
|
|
it is not contained in a bundle). if the resource could not be found, nil is returned.
|
2014-07-14 13:34:14 -04:00
|
|
|
for any other type of response (errors), an exception is thrown.
|
2014-07-08 13:52:54 -04:00
|
|
|
|
|
|
|
a relative url can be used to identify the resource to be retrieved, or a resource type,
|
|
|
|
id and optional version number can be used.
|
2014-07-04 09:04:45 -04:00
|
|
|
|
|
|
|
reference:
|
|
|
|
read: http://hl7.org/implement/standards/fhir/http.html#read
|
2014-07-08 13:52:54 -04:00
|
|
|
vread: http://hl7.org/implement/standards/fhir/http.html#vread
|
|
|
|
relative url: http://hl7.org/implement/standards/fhir/references.html#atom-rel"
|
|
|
|
([base-url relative-resource-url]
|
|
|
|
(try
|
2014-07-08 14:24:55 -04:00
|
|
|
(fhir-request :get
|
2014-07-08 13:52:54 -04:00
|
|
|
base-url
|
|
|
|
relative-resource-url)
|
|
|
|
(catch ExceptionInfo ex
|
2014-07-14 11:03:51 -04:00
|
|
|
(let [http-status (:status (ex-data ex))]
|
2014-07-11 15:16:48 -04:00
|
|
|
; TODO: do we want to handle 410 differently? either way, the resource is not available
|
|
|
|
; though, a 410 could indicate to the caller that it might be available under a
|
|
|
|
; previous version ...
|
|
|
|
(if-not (or (= http-status 404)
|
|
|
|
(= http-status 410))
|
|
|
|
(throw ex))))))
|
2014-07-08 13:52:54 -04:00
|
|
|
([base-url type id & {:keys [version]}]
|
|
|
|
(let [resource-name (->fhir-resource-name type)
|
|
|
|
url-components (if version
|
|
|
|
["/" resource-name id "_history" version]
|
|
|
|
["/" resource-name id])]
|
|
|
|
(get-resource base-url (apply join-paths url-components)))))
|
|
|
|
|
|
|
|
(defn get-relative-resource
|
|
|
|
"gets a single resource from a FHIR server. the server to be queried will be taken from the
|
2014-07-14 13:34:14 -04:00
|
|
|
'fhir-base' link in the provided bundle. an exception is thrown if an error response is
|
|
|
|
received."
|
2014-07-08 13:52:54 -04:00
|
|
|
[bundle relative-url]
|
|
|
|
(if bundle
|
|
|
|
(let [base-url (->> (:link bundle)
|
|
|
|
(filter #(= "fhir-base" (:rel %)))
|
|
|
|
(first)
|
|
|
|
:href)]
|
|
|
|
(get-resource base-url relative-url))))
|
2014-07-04 09:04:45 -04:00
|
|
|
|
2014-07-04 09:34:37 -04:00
|
|
|
(defn get-resource-bundle
|
2014-07-08 11:16:39 -04:00
|
|
|
"gets a single resource from a FHIR server. the returned resource will be contained in a
|
|
|
|
bundle. if the resource could not be found, a bundle containing zero resources is returned.
|
2014-07-14 13:34:14 -04:00
|
|
|
an exception is thrown if an error response is received.
|
2014-07-04 13:56:43 -04:00
|
|
|
|
|
|
|
reference:
|
|
|
|
bundles: http://hl7.org/implement/standards/fhir/extras.html#bundle"
|
2014-07-04 09:34:37 -04:00
|
|
|
[base-url type id]
|
|
|
|
(let [resource-name (->fhir-resource-name type)
|
|
|
|
url-components ["/" resource-name]]
|
2014-07-08 14:24:55 -04:00
|
|
|
(fhir-request :get
|
2014-07-04 09:34:37 -04:00
|
|
|
base-url
|
|
|
|
(apply join-paths url-components)
|
2014-07-08 14:24:55 -04:00
|
|
|
:params {:_id id})))
|
2014-07-04 09:34:37 -04:00
|
|
|
|
2014-07-04 09:04:45 -04:00
|
|
|
(defn search
|
|
|
|
"searches for resources on a FHIR server. multiple parameters are ANDed together. use of the search
|
|
|
|
operator helper functions is encouraged to ensure proper escaping/encoding of search parameters.
|
2014-07-04 13:56:43 -04:00
|
|
|
the results of this function can be passed to fetch-next-page or fetch-all to collect resources
|
2014-07-14 13:34:14 -04:00
|
|
|
returned in paged search results easier. an exception is thrown if an error response is received.
|
2014-07-04 09:04:45 -04:00
|
|
|
|
|
|
|
reference:
|
|
|
|
search: http://hl7.org/implement/standards/fhir/http.html#search"
|
2014-07-04 18:55:00 -04:00
|
|
|
[base-url type where & params]
|
2014-07-04 09:04:45 -04:00
|
|
|
(let [resource-name (->fhir-resource-name type)
|
|
|
|
url-components ["/" resource-name]]
|
2014-07-08 14:24:55 -04:00
|
|
|
(fhir-request :get
|
2014-07-04 09:04:45 -04:00
|
|
|
base-url
|
|
|
|
(apply join-paths url-components)
|
2014-07-08 14:24:55 -04:00
|
|
|
:params (merge
|
|
|
|
(search-params->query-map where)
|
|
|
|
(apply hash-map (if (and (seq? params)
|
|
|
|
(= 1 (count params)))
|
|
|
|
(first params)
|
|
|
|
params))))))
|
2014-07-08 11:33:13 -04:00
|
|
|
|
|
|
|
(defn search-and-fetch
|
|
|
|
"same as search, but automatically fetches all pages of resources returning a single bundle
|
2014-07-14 13:34:14 -04:00
|
|
|
that contains all search results. an exception is thrown if an error response is received."
|
2014-07-08 11:33:13 -04:00
|
|
|
[base-url type where & params]
|
|
|
|
(fetch-all
|
|
|
|
(search base-url type where params)))
|
2014-07-08 08:30:36 -04:00
|
|
|
|
2014-07-14 11:26:25 -04:00
|
|
|
(defn create
|
2014-07-18 13:34:47 -04:00
|
|
|
"creates a new resource. returns the created resource if successful and the server response
|
|
|
|
contained a 'Location' header, otherwise returns nil. throws an exception otherwise.
|
2014-07-14 13:34:14 -04:00
|
|
|
|
|
|
|
reference:
|
|
|
|
create: http://hl7.org/implement/standards/fhir/http.html#create"
|
2014-07-14 11:26:25 -04:00
|
|
|
[base-url type resource]
|
|
|
|
(let [resource-name (->fhir-resource-name type)
|
|
|
|
uri-components ["/" resource-name]]
|
|
|
|
(fhir-request :post
|
|
|
|
base-url
|
|
|
|
(apply join-paths uri-components)
|
|
|
|
:body resource)))
|
|
|
|
|
2014-07-14 15:22:17 -04:00
|
|
|
(defn update
|
2014-07-18 13:34:47 -04:00
|
|
|
"updates an existing resource. returns the updated resource if successful and the server
|
|
|
|
response contained a 'Location' header, otherwise returns nil. throws an exception if
|
|
|
|
an error response was received.
|
|
|
|
|
|
|
|
reference:
|
|
|
|
update: http://hl7.org/implement/standards/fhir/http.html#update"
|
|
|
|
[base-url type id resource & {:keys [version]}]
|
2014-07-14 15:22:17 -04:00
|
|
|
(let [resource-name (->fhir-resource-name type)
|
|
|
|
uri-components (if version
|
|
|
|
["/" resource-name id "_history" version]
|
|
|
|
["/" resource-name id])]
|
|
|
|
(fhir-request :put
|
|
|
|
base-url
|
|
|
|
(apply join-paths uri-components)
|
|
|
|
:body resource)))
|
|
|
|
|
2014-07-08 08:30:36 -04:00
|
|
|
;(def server-url "http://fhir.healthintersections.com.au/open")
|
|
|
|
;(def server-url "http://spark.furore.com/fhir")
|
2014-07-14 13:37:06 -04:00
|
|
|
;(def server-url "http://fhirtest.uhn.ca/base")
|