dependencies
| (this space intentionally left almost blank) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
namespaces
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace defines the REST API handlers for collection resources. | (ns cmr.opendap.app.handler.collection (:require [cheshire.core :as json] [clojure.core.async :as async] [clojure.java.io :as io] [cmr.opendap.auth.token :as token] [cmr.opendap.components.config :as config] [cmr.opendap.errors :as errors] [cmr.opendap.ous.core :as ous] [cmr.opendap.http.response :as response] [org.httpkit.server :as server] [org.httpkit.timer :as timer] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
OUS Handlers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Private function for creating OPeNDAP URLs when supplied with an HTTP GET. | (defn- generate
[component request user-token concept-id data]
(log/debug "Generating URLs based on HTTP GET ...")
(->> data
(merge {:collection-id concept-id})
(ous/get-opendap-urls component user-token)
(response/json request))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Private function for creating OPeNDAP URLs when supplied with an HTTP GET. | (defn- generate-via-get
[component request user-token concept-id]
(log/debug "Generating URLs based on HTTP GET ...")
(->> request
:params
(generate component request user-token concept-id))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Private function for creating OPeNDAP URLs when supplied with an HTTP POST. | (defn- generate-via-post
[component request user-token concept-id]
(->> request
:body
(slurp)
(#(json/parse-string % true))
(generate component request user-token concept-id))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
XXX | (defn unsupported-method
[request]
{:error errors/not-implemented}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
XXX | (defn generate-urls
[component]
(fn [request]
(log/debug "Method-dispatching for URLs generation ...")
(log/trace "request:" request)
(let [user-token (token/extract request)
concept-id (get-in request [:path-params :concept-id])]
(case (:request-method request)
:get (generate-via-get component request user-token concept-id)
:post (generate-via-post component request user-token concept-id)
(unsupported-method request))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
XXX | (defn batch-generate
[component]
;; XXX how much can we minimize round-tripping here?
;; this may require creating divergent logic/impls ...
;; XXX This is being tracked in CMR-4864
(fn [request]
{:error errors/not-implemented})) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn stream-urls
[component]
(fn [request]
(let [heartbeat (config/streaming-heartbeat component)
timeout (config/streaming-timeout component)
iterations (Math/floor (/ timeout heartbeat))]
(log/debug "Processing stream request ...")
(server/with-channel request channel
(log/debug "Setting 'on-close' callback ...")
(server/on-close channel
(fn [status]
(println "Channel closed; status " status)))
(let [result-channel (async/thread
((generate-urls component) request))]
(log/trace "Starting loop ...")
(async/go-loop [id 0]
(log/trace "Loop id:" id)
(if-let [result (async/<! result-channel)]
(do
(log/trace "Result:" result)
(server/send! channel result)
(server/close channel)
(when (< id iterations)
(timer/schedule-task
(* id heartbeat) ;; send a message every heartbeat period
(log/trace "\tSending 0-byte placeholder chunk to client ...")
(server/send! channel
{:status 202}
false))
(recur (inc id))))))
(timer/schedule-task timeout (server/close channel))))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace defines the handlers for general resources. Simple handlers will only need to make a call to a library and then have that data prepared for the client by standard response function. More complex handlers will need to perform additional tasks. For example, in order of increasing complexity: * utilize non-default, non-trivial response functions * operate on the obtained data with various transformations, including extracting form data, query strings, etc. * take advantage of middleware functions that encapsulate complicated business logic | (ns cmr.opendap.app.handler.core (:require [clojure.java.io :as io] [clojusc.twig :as twig] [cmr.opendap.health :as health] [cmr.opendap.http.response :as response] [ring.middleware.file :as file-middleware] [ring.util.codec :as codec] [ring.util.http-response] [ring.util.response :as ring-response] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Admin Handlers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn health
[component]
(fn [request]
(->> component
health/components-ok?
(response/json request)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def ping
(fn [request]
(response/json request {:result :pong}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Utility Handlers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn status
[status-keyword]
(fn [request]
((ns-resolve 'ring.util.http-response (symbol (name status-keyword))) {}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def ok
(fn [request]
(response/ok request))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn text-file
[filepath]
(fn [request]
(if-let [file-resource (io/resource filepath)]
(response/text request (slurp file-resource))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn html-file
[filepath]
(fn [request]
(if-let [file-resource (io/resource filepath)]
(response/html request (slurp file-resource))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn dynamic-page [page-fn data] #(page-fn % data)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn permanent-redirect
[location]
(fn [request]
(ring-response/redirect location :moved-permanently))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace defines the handlers for the cache REST API resources. | (ns cmr.opendap.app.handler.cache (:require [clojure.java.io :as io] [clojusc.twig :as twig] [cmr.opendap.components.caching :as caching] [cmr.opendap.http.response :as response] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn lookup-all
[component]
(fn [request]
(->> component
caching/lookup-all
(response/json request)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn evict-all
[component]
(fn [request]
(log/debug "Evicting all cached items ...")
(->> component
caching/evict-all
(response/json request)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn lookup
[component]
(fn [request]
(let [item-key (get-in request [:path-params :item-key])]
(response/json
request
(caching/lookup component item-key))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn evict
[component]
(fn [request]
(let [item-key (get-in request [:path-params :item-key])]
(log/debugf "Evicting value cached at key %s ..." item-key)
(caching/evict component item-key)
(response/json
request
(caching/lookup component item-key))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Custom ring middleware for CMR OPeNDAP. | (ns cmr.opendap.app.middleware (:require [clojure.string :as string] [cmr.opendap.auth.core :as auth] [cmr.opendap.components.config :as config] [cmr.opendap.http.request :as request] [cmr.opendap.http.response :as response] [cmr.opendap.site.pages :as pages] [cmr.opendap.app.routes.rest.core :as rest-routes] [reitit.ring :as ring] [ring.middleware.content-type :as ring-ct] [ring.middleware.defaults :as ring-defaults] [ring.middleware.file :as ring-file] [ring.middleware.not-modified :as ring-nm] [ring.util.response :as ring-response] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Ring-based middleware for supporting CORS requests. | (defn wrap-cors
[handler]
(fn [req]
(response/cors req (handler req)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Ring-based middleware forremoving a single trailing slash from the end of the URI, if present. | (defn wrap-trailing-slash
[handler]
(fn [req]
(let [uri (:uri req)]
(handler (assoc req :uri (if (and (not= "/" uri)
(.endsWith uri "/"))
(subs uri 0 (dec (count uri)))
uri)))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn wrap-fallback-content-type
[handler default-content-type]
(fn [req]
(condp = (:content-type req)
nil (assoc-in (handler req)
[:headers "Content-Type"]
default-content-type)
"application/octet-stream" (assoc-in (handler req)
[:headers "Content-Type"]
default-content-type)
:else (handler req)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn wrap-directory-resource
([handler system]
(wrap-directory-resource handler system "text/html"))
([handler system content-type]
(fn [req]
(let [response (handler req)]
(cond
(contains? (config/http-index-dirs system)
(:uri req))
(ring-response/content-type response content-type)
:else
response))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn wrap-base-url-subs
[handler system]
(fn [req]
(let [response (handler req)]
(if (contains? (config/http-replace-base-url system)
(:uri req))
(assoc response
:body
(string/replace
(slurp (:body response))
(re-pattern (config/http-rest-docs-base-url-template system))
(config/opendap-url system)))
response)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn wrap-resource
[handler system]
(let [docs-resource (config/http-docs system)
assets-resource (config/http-assets system)
compound-handler (-> handler
(ring-file/wrap-file
docs-resource {:allow-symlinks? true})
(ring-file/wrap-file
assets-resource {:allow-symlinks? true})
(wrap-directory-resource system)
(wrap-base-url-subs system)
(ring-ct/wrap-content-type)
(ring-nm/wrap-not-modified))]
(fn [req]
(if (contains? (config/http-skip-static system)
(:uri req))
(handler req)
(compound-handler req))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn wrap-not-found
[handler system]
(fn [req]
(let [response (handler req)
status (:status response)]
(cond (string/includes? (:uri req) "stream")
(do
(log/debug "Got streaming response; skipping 404 checks ...")
response)
(or (= 404 status) (nil? status))
(do
(when (nil? status)
(log/debug "Got nil status in not-found middleware ..."))
(assoc (pages/not-found
req
{:base-url (config/opendap-url system)})
:status 404))
:else
response)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Ring-based middleware for supporting the protection of routes using the CMR Access Control service and CMR Legacy ECHO support. In particular, this wrapper allows for the protection of routes by both roles as well as concept-specific permissions. This is done by annotating the routes per the means described in the reitit library's documentation. | (defn wrap-auth
[handler system]
(fn [req]
(log/debug "Running perms middleware ...")
(auth/check-route-access system handler req))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn reitit-auth
[system]
"This auth middleware is specific to reitit, providing the data structure
necessary that will allow for the extraction of roles and permissions
settings from the request.
For more details, see the docstring above for `wrap-auth`."
{:data
{:middleware [#(wrap-auth % system)]}}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn wrap-api-version-dispatch
[site-routes system opts]
(fn [req]
(log/trace "Got site-routes:" (vec site-routes))
(let [api-version (request/accept-api-version system req)
routes (concat site-routes (rest-routes/all system api-version))
handler (ring/ring-handler (ring/router routes opts))
header (format "%s; format=%s"
(request/accept-media-type system req)
(request/accept-format system req))]
(log/debug "API version:" api-version)
(log/trace "Made routes:" (vec routes))
(response/version-media-type (handler req) header)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.app.core (:require [clojure.java.io :as io] [cmr.opendap.app.handler.core :as handler] [cmr.opendap.app.middleware :as middleware] [cmr.opendap.app.routes.site :as site-routes] [cmr.opendap.components.config :as config] [ring.middleware.defaults :as ring-defaults] [reitit.ring :as ring] [taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn main
[httpd-component]
(let [docs-resource (config/http-docs httpd-component)
assets-resource (config/http-assets httpd-component)]
(-> httpd-component
site-routes/all
(middleware/wrap-api-version-dispatch
httpd-component
(middleware/reitit-auth httpd-component))
(ring-defaults/wrap-defaults ring-defaults/api-defaults)
(middleware/wrap-resource httpd-component)
middleware/wrap-trailing-slash
middleware/wrap-cors
(middleware/wrap-not-found httpd-component)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace defines the REST routes provided by this service. Upon idnetifying a particular request as matching a given route, work is then handed off to the relevant request handler function. | (ns cmr.opendap.app.routes.site (:require [cmr.opendap.components.config :as config] [cmr.opendap.app.handler.cache :as cache-handler] [cmr.opendap.app.handler.collection :as collection-handler] [cmr.opendap.app.handler.core :as core-handler] [cmr.opendap.health :as health] [cmr.opendap.site.pages :as pages] [reitit.ring :as ring] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
CMR OPeNDAP Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn main
[httpd-component]
[["/opendap" {
:get (core-handler/dynamic-page
pages/home
{:base-url (config/opendap-url httpd-component)})
:head core-handler/ok}]]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Note that these routes only cover part of the docs; the rest are supplied via static content from specific directories (done in middleware). | (defn docs
[httpd-component]
[["/opendap/docs" {
:get (core-handler/dynamic-page
pages/opendap-docs
{:base-url (config/opendap-url httpd-component)})}]]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Static & Redirect Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn redirects
[httpd-component]
[["/opendap/robots.txt" {
:get (core-handler/permanent-redirect
(str (config/get-search-url httpd-component)
"/robots.txt"))}]]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn static
[httpd-component]
[;; Google verification files
["/opendap/googled099d52314962514.html" {
:get (core-handler/text-file
"public/verifications/googled099d52314962514.html")}]]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Assembled Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn all [httpd-component] (concat (main httpd-component) (docs httpd-component) (redirects httpd-component) (static httpd-component))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace defines the Version 1 REST routes provided by this service. Upon idnetifying a particular request as matching a given route, work is then handed off to the relevant request handler function. | (ns cmr.opendap.app.routes.rest.v1 (:require [cmr.opendap.components.config :as config] [cmr.opendap.app.handler.collection :as collection-handler] [cmr.opendap.app.handler.core :as core-handler] [cmr.opendap.health :as health] [cmr.opendap.site.pages :as pages] [reitit.ring :as ring] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
REST API Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn ous-api
[httpd-component]
[["/opendap/ous/collections" {
:post {:handler collection-handler/batch-generate
;; XXX CMR-4864, CMR-4863
;; Protecting collections will be a little different than
;; protecting a single collection, since the concept-id isn't in
;; the path-params. Instead, we'll have to parse the body,
;; extract the concepts ids from that, create an ACL query
;; containing multiple concept ids, and then check those results.
;; :permission #{...?}
}
:options core-handler/ok}]
["/opendap/ous/collection/:concept-id" {
:get {:handler (collection-handler/generate-urls httpd-component)
:permissions #{:read}}
:post {:handler (collection-handler/generate-urls httpd-component)
:permissions #{:read}}
:options core-handler/ok}]
["/opendap/ous/streaming-collection/:concept-id" {
:get (collection-handler/stream-urls httpd-component)}]]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn admin-api
[httpd-component]
[["/opendap/health" {
:get (core-handler/health httpd-component)
:options core-handler/ok}]
["/opendap/ping" {
:get {:handler core-handler/ping
:roles #{:admin}}
:post {:handler core-handler/ping
:roles #{:admin}}
:options core-handler/ok}]]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Testing Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def testing
[["/testing/401" {:get (core-handler/status :unauthorized)}]
["/testing/403" {:get (core-handler/status :forbidden)}]
["/testing/404" {:get (core-handler/status :not-found)}]
["/testing/405" {:get (core-handler/status :method-not-allowed)}]
["/testing/500" {:get (core-handler/status :internal-server-error)}]
["/testing/503" {:get (core-handler/status :service-unavailable)}]]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Assembled Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn all [httpd-component] (concat (ous-api httpd-component) (admin-api httpd-component) testing)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace defines the Version 2 REST routes provided by this service. Upon idnetifying a particular request as matching a given route, work is then handed off to the relevant request handler function. | (ns cmr.opendap.app.routes.rest.v2 (:require [cmr.opendap.components.config :as config] [cmr.opendap.app.handler.cache :as cache-handler] [cmr.opendap.app.handler.collection :as collection-handler] [cmr.opendap.app.handler.core :as core-handler] [cmr.opendap.app.routes.rest.v1 :as routes-v1] [cmr.opendap.health :as health] [cmr.opendap.site.pages :as pages] [reitit.ring :as ring] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
REST API Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn admin-api
[httpd-component]
(concat
[["/opendap/cache" {
:get {:handler (cache-handler/lookup-all httpd-component)
:roles #{:admin}}
:delete {:handler (cache-handler/evict-all httpd-component)
:roles #{:admin}}}]
["/opendap/cache/:item-key" {
:get {:handler (cache-handler/lookup httpd-component)
:roles #{:admin}}
:delete {:handler (cache-handler/evict httpd-component)
:roles #{:admin}}}]]
(routes-v1/admin-api httpd-component))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Assembled Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn all [httpd-component] (concat (routes-v1/ous-api httpd-component) (admin-api httpd-component) routes-v1/testing)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.app.routes.rest.core (:require [cmr.opendap.app.routes.rest.v1 :as v1] [cmr.opendap.app.routes.rest.v2 :as v2] [taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn all
[httpd-component version]
(case (keyword version)
:v1 (v1/all httpd-component)
(v2/all httpd-component))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.granule (:require [clojure.string :as string] [cmr.opendap.components.config :as config] [cmr.opendap.const :as const] [cmr.opendap.errors :as errors] [cmr.opendap.http.request :as request] [cmr.opendap.http.response :as response] [cmr.opendap.ous.query.results :as results] [cmr.opendap.ous.util.core :as ous-util] [cmr.opendap.util :as util] [ring.util.codec :as codec] [taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn build-include
[gran-ids]
(string/join
"&"
(conj
(map #(str (codec/url-encode "concept_id[]")
"="
%)
gran-ids)
(str "page_size=" (count gran-ids))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn build-exclude
[component gran-ids]
(string/join
"&"
(conj
(map #(str (codec/url-encode "exclude[echo_granule_id][]")
"="
%)
gran-ids)
;; We don't know how many granule ids will be involved in an exclude,
;; so we use CMR's max page size.
(str "page_size=" (config/cmr-max-pagesize component))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Build the query string for querying granles, bassed upon the options passed in the parameters. | (defn build-query
[component params]
(let [coll-id (:collection-id params)
gran-ids (util/remove-empty (:granules params))
exclude? (:exclude-granules params)
bounding-box (:bounding-box params)
temporal (:temporal params)]
(str "collection_concept_id=" coll-id
(when (seq gran-ids)
(str "&"
(if exclude?
(build-exclude component gran-ids)
(build-include gran-ids))))
(when (seq bounding-box)
(str "&bounding_box="
(ous-util/seq->str bounding-box)))
(when (seq temporal)
(str "&"
(ous-util/temporal-seq->cmr-query temporal)))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Given a data structure with :collection-id, :granules, and :exclude-granules keys, get the metadata for the desired granules. Which granule metadata is returned depends upon the values of :granules and :exclude-granules | (defn async-get-metadata
[component search-endpoint user-token params]
(let [url (str search-endpoint "/granules")
payload (build-query component params)]
(log/debug "Granules query CMR URL:" url)
(log/debug "Granules query CMR payload:" payload)
(request/async-post
url
(-> {}
(request/add-token-header user-token)
(request/add-accept "application/json")
(request/add-form-ct)
(request/add-payload payload))
response/json-handler))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-metadata
[promise]
(let [rslts @promise]
(if (errors/erred? rslts)
(do
(log/error errors/granule-metadata)
rslts)
(do
(log/debug "Got results from CMR granule search:"
(results/elided rslts))
(log/trace "Remaining results:" (results/remaining-items rslts))
(get-in rslts [:feed :entry]))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-metadata
[component search-endpoint user-token params]
(let [promise (async-get-metadata component search-endpoint user-token params)]
(extract-metadata promise))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The criteria defined in the prototype was to iterate through the links, only examining those links that were not 'inherited', and find the one whose :rel value matched a particular string. It is currently unclear what the best criteria for this decision is. XXX The following may need to change once CMR-4912 is addressed ... | (defn match-datafile-link
[link-data]
(log/trace "Link data:" link-data)
(let [rel (:rel link-data)]
(and (not (:inherited link-data))
(= const/datafile-link-rel rel)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-datafile-link
[granule-entry]
(log/trace "Granule entry: " granule-entry)
(let [link (->> (:links granule-entry)
(filter match-datafile-link)
first)
gran-id (:id granule-entry)]
(if link
{:granule-id gran-id
:link-rel (:rel link)
:link-href (:href link)}
{:errors [errors/empty-gnl-data-file-url
(when gran-id
(format errors/problem-granules gran-id))]}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.util.geog (:require [cmr.opendap.const :as const] [taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn adjusted-lon
([lon]
(adjusted-lon lon const/default-lat-lon-resolution))
([lon resolution]
(- (* lon resolution)
(* const/default-lon-lo resolution)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn adjusted-lat
([lat]
(adjusted-lat lat const/default-lat-lon-resolution))
([lat resolution]
(- (* lat resolution)
(* const/default-lat-lo resolution)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
OPeNDAP indices are 0-based, thus gridded longitudinal data with 1x resolution is stored at indices from 0 to 359 and similar latitudinal data is stored at indices from 0 to 179. The max values for lat and lon are stored in the UMM-Var records as part of the dimensions. Sometimes those values are pre-decremented for use in OPeNDAP, sometimes not (e.g., sometimes max longitude is given as 359, sometimes as 360). This function attempts to ensure a consistent use of decremented max values for indices. | (defn offset-index
([max default-max]
(offset-index max default-max const/default-lat-lon-resolution))
([max default-max resolution]
(if (< max (* default-max resolution))
max
(dec max)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Longitude goes from -180 to 180 and latitude from -90 to 90. However, when referencing data in OPeNDAP arrays, 0-based indices are needed. Thus in order to get indices that match up with degrees, our longitude needs to be phase-shifted by 180 degrees, latitude by 90 degrees. | (defn phase-shift
[degrees-max default-abs-degrees-max default-degrees-max degrees adjust-fn round-fn]
(let [res (Math/ceil (/ degrees-max default-abs-degrees-max))]
(log/trace "Got degrees-max:" degrees-max)
(log/trace "Got degrees:" degrees)
(log/trace "Got resolution:" res)
(-> (/ (* (offset-index degrees-max default-abs-degrees-max res)
(adjust-fn degrees res))
(adjust-fn default-degrees-max res))
round-fn
int))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn lon-lo-phase-shift [lon-max lon-lo] (phase-shift lon-max const/default-lon-abs-hi const/default-lon-hi lon-lo adjusted-lon #(Math/floor %))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn lon-hi-phase-shift [lon-max lon-hi] (phase-shift lon-max const/default-lon-abs-hi const/default-lon-hi lon-hi adjusted-lon #(Math/ceil %))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This is used for reading values from OPeNDAP where -90N is stored at the zero (first) index in the array. | (defn lat-lo-phase-shift [lat-max lat-lo] (phase-shift lat-max const/default-lat-abs-hi const/default-lat-hi lat-lo adjusted-lat #(Math/floor %))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This is used for reading values from OPeNDAP where -90N is stored at the zero (first) index in the array. | (defn lat-hi-phase-shift [lat-max lat-hi] (phase-shift lat-max const/default-lat-abs-hi const/default-lat-hi lat-hi adjusted-lat #(Math/ceil %))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This is used for reading values from OPeNDAP where 90N is stored at the zero (first) index in the array. Note that this must also be used in conjunction with the hi and lo values
for latitude in the OPeNDAP lookup array being swapped (see
| (defn lat-lo-phase-shift-reversed
[lat-max lat-lo]
(let [res (Math/ceil (/ lat-max const/default-lat-abs-hi))]
(int
(- (offset-index lat-max const/default-lat-abs-hi res)
(lat-lo-phase-shift lat-max lat-lo))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This is used for reading values from OPeNDAP where 90N is stored at the zero (first) index in the array. Note that this must also be used in conjunction with the hi and lo values
for latitude in the OPeNDAP lookup array being swapped (see
| (defn lat-hi-phase-shift-reversed
[lat-max lat-lo]
(let [res (Math/ceil (/ lat-max const/default-lat-abs-hi))]
(int
(- (offset-index lat-max const/default-lat-abs-hi res)
(lat-hi-phase-shift lat-max lat-lo))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.util.core (:require [clojure.string :as string] [ring.util.codec :as codec])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn normalize-param
[param]
(-> param
name
(string/replace "_" "-")
(string/lower-case)
keyword)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn normalize-params
[params]
(->> params
(map (fn [[k v]] [(normalize-param k) v]))
(into {}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn ->base-coll
[data]
(cond (nil? data) []
(empty? data) []
:else data)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn ->coll
[data]
(let [coll (->base-coll data)]
(if (string? coll)
[coll]
coll))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn split-comma->coll
[data]
(let [coll (->base-coll data)]
(if (string? coll)
(string/split data #",")
coll))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn split-comma->sorted-coll [data] (sort (split-comma->coll data))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn seq->str [data] (string/join "," data)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn temporal-seq->cmr-query
[data]
(let [sep (str (codec/url-encode "temporal[]") "=")]
(str sep
(string/join (str "&" sep)
(map codec/url-encode data))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn bounding-box->subset [[lon-lo lat-lo lon-hi lat-hi]] [(format "lat(%s,%s)" lat-lo lat-hi) (format "lon(%s,%s)" lon-lo lon-hi)]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-matches
[regex elems]
(->> elems
(map (comp rest (partial re-find regex)))
(remove empty?)
first)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn subset->bounding-lat
[elems]
(get-matches
(re-pattern (str ".*lat\\("
"\\s*(-?[0-9]+\\.?[0-9]*)\\s*,"
"\\s*(-?[0-9]+\\.?[0-9]*)\\s*"))
elems)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn subset->bounding-lon
[elems]
(get-matches
(re-pattern (str ".*lon\\("
"\\s*(-?[0-9]+\\.?[0-9]*)\\s*,"
"\\s*(-?[0-9]+\\.?[0-9]*)\\s*"))
elems)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
In the CMR and EDSC, a bounding box is defined by the lower-left corner
to the upper-right, furthermore, they defined this as a flattened list,
ordering with longitude first. As such, a bounding box is of the form:
This is the form that this function returns. | (defn subset->bounding-box
[elems]
(let [[lon-lo lon-hi] (subset->bounding-lon elems)
[lat-lo lat-hi] (subset->bounding-lat elems)]
(map #(Float/parseFloat %) [lon-lo lat-lo lon-hi lat-hi]))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn bounding-box-lat [[_ lower-latitude _ upper-latitude]] [lower-latitude upper-latitude]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn bounding-box-lon [[lower-longitude _ upper-longitude _]] [lower-longitude upper-longitude]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn coverage->granules
[coverage]
(let [ids (filter #(string/starts-with? % "G") coverage)]
(when (seq ids)
ids))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn coverage->collection
[coverage]
(let [id (filter #(string/starts-with? % "C") coverage)]
(when (seq id)
(first id)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.collection (:require [clojure.string :as string] [cmr.opendap.components.config :as config] [cmr.opendap.http.request :as request] [cmr.opendap.http.response :as response] [taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Defaults ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def defualt-processing-level "3") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def supported-processing-levels
#{"3" "4"}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Utility/Support Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
XXX - The need for this function is absurd: "Not Provided" and "NA" are considered valid values for collection proccessing level. CMR OPeNDAP currently only supports level 3 and 4, and one of the supported collections is level 3, but has a proccessing level value set to "Not Provided". Thus, this hack. XXX - This is being tracked in CMR-4989. | (defn sanitize-processing-level
[level]
(if (or (= "NA" level)
(= "Not Provided" level))
defualt-processing-level
level)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Collection API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn build-query [params] (str "concept_id=" (:collection-id params))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Given a data structure with :collection-id, get the metadata for the associated collection. | (defn async-get-metadata
[search-endpoint user-token params]
(let [url (str search-endpoint
"/collections?"
(build-query params))]
(log/debug "Collection query to CMR:" url)
(request/async-get
url
(-> {}
(request/add-token-header user-token)
(request/add-accept "application/json"))
response/json-handler))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-metadata
[promise]
(let [results @promise]
(log/trace "Got results from CMR granule collection:" results)
(first (get-in results [:feed :entry])))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-metadata
[search-endpoint user-token params]
(let [promise (async-get-metadata search-endpoint user-token params)]
(extract-metadata promise))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-variable-ids [entry] (sort (get-in entry [:associations :variables]))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-service-ids [entry] (sort (get-in entry [:associations :services]))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-processing-level
[entry]
(log/trace "Collection entry:" entry)
(sanitize-processing-level
(or (:processing_level_id entry)
(get-in entry [:umm :ProcessingLevel :Id])
defualt-processing-level))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.core (:require [clojure.set :as set] [clojure.string :as string] [cmr.opendap.components.config :as config] [cmr.opendap.errors :as errors] [cmr.opendap.ous.collection :as collection] [cmr.opendap.ous.granule :as granule] [cmr.opendap.ous.query.params.core :as params] [cmr.opendap.ous.query.results :as results] [cmr.opendap.ous.service :as service] [cmr.opendap.ous.util.core :as ous-util] [cmr.opendap.ous.variable :as variable] [cmr.opendap.util :as util] [cmr.opendap.validation :as validation] [taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Utility/Support Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn bbox->bounding-info
;; XXX coll is required as an arg here because it's needed in a
;; workaround for different data sets using different starting
;; points for their indices in OPeNDAP
;;
;; XXX This is being tracked in CMR-4982
[coll bounding-box]
(variable/map->BoundingInfo
{:bounds bounding-box
:opendap (variable/create-opendap-bounds
bounding-box
{:reversed? (variable/lat-reversed? coll)})})) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn format-opendap-lat-lon
;; XXX coll is required as an arg here because it's needed in a
;; workaround for different data sets using different starting
;; points for their indices in OPeNDAP
;;
;; XXX This is being tracked in CMR-4982
[coll bounding-infos bounding-box]
(if-let [bounding-info (first bounding-infos)]
(variable/format-opendap-lat-lon bounding-info)
(variable/format-opendap-lat-lon
(bbox->bounding-info coll bounding-box)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn bounding-infos->opendap-query
;; XXX coll is required as an arg here because it's needed in a
;; workaround for different data sets using different starting
;; points for their indices in OPeNDAP
;;
;; XXX This is being tracked in CMR-4982
([coll bounding-infos]
(bounding-infos->opendap-query coll bounding-infos nil))
([coll bounding-infos bounding-box]
(when (seq bounding-infos)
(str
(->> bounding-infos
(map variable/format-opendap-bounds)
(string/join ",")
(str "?"))
","
(format-opendap-lat-lon coll bounding-infos bounding-box))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
XXX The | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def fallback-pattern #"(.*)(/datapool/DEV01)(.*)") (def fallback-replacement "/opendap/DEV01/user") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn data-file->opendap-url
[data-file]
(let [data-url (:link-href data-file)]
(log/debug "Data file:" data-file)
(cond (string/includes? data-url fallback-replacement)
(do
(log/debug (str "Data file already has the expected OPeNDAP URL; "
"skipping replacement ..."))
data-url)
(re-matches fallback-pattern data-url)
(do
(log/debug
"Attempting Granule URL match/replace ...")
(string/replace data-url
fallback-pattern
(str "$1" fallback-replacement "$3")))
:else
(let [msg (format errors/no-matching-service-pattern
fallback-pattern
data-url)]
(log/error msg)
{:errors [msg]})))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn replace-double-slashes [url] (string/replace url #"(?<!(http:|https:))[//]+" "/")) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn data-files->opendap-urls
[params data-files query-string]
(when data-files
(let [urls (map (comp replace-double-slashes
data-file->opendap-url)
data-files)]
(if (errors/any-erred? urls)
(do
(log/error "Some problematic urls:" (vec urls))
(apply errors/collect urls))
(map #(str % "." (:format params) query-string) urls))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn apply-level-conditions
[coll params]
(let [level (collection/extract-processing-level coll)]
; errors/
(log/info "Got level:" level)
(if (contains? collection/supported-processing-levels level)
params
{:errors [errors/unsupported-processing-level
(format errors/problem-processing-level
level
(:id coll))]}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There are several variable and bounding scenarios we need to consider:
| (defn apply-bounding-conditions
[search-endpoint user-token coll {:keys [bounding-box variables] :as params}]
(log/debugf (str "Applying bounding conditions with bounding box %s and "
"variable ids %s ...")
bounding-box
variables)
(cond
;; Condition 1 - no spatial subsetting and no variables
(and (nil? bounding-box) (empty? variables))
[]
;; Condition 2 - variables but no spatial subsetting
(and (nil? bounding-box) (seq variables))
(variable/get-metadata search-endpoint user-token params)
;; Condition 3 - variables and spatial subsetting
(and bounding-box (seq variables))
(variable/get-metadata search-endpoint user-token params)
;; Condition 4 - spatial subsetting but no variables
(and bounding-box (empty? variables))
(variable/get-metadata search-endpoint
user-token
(assoc params :variables (collection/extract-variable-ids coll))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Stages for URL Generation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; The various stage functions below were originally called as part of a In particular: Functions which depend only upon the parameters (or parsing of those parameters) are placed in the first stage. Functions which depend upon either the parameters or the results of the first stage are placed in the second stage, etc. The reason for this was to make it very clear when various functions could be called as late as possible, and only call those which were absolutely necessary at a given point. And the reason for that was so the code could be properly prepared for async execution. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn stage1
[component search-endpoint user-token raw-params]
(log/debug "Starting stage 1 ...")
(let [params (params/parse raw-params)
bounding-box (:bounding-box params)
valid-lat (when bounding-box
(validation/validate-latitude
(ous-util/bounding-box-lat bounding-box)))
valid-lon (when bounding-box
(validation/validate-longitude
(ous-util/bounding-box-lon bounding-box)))
grans-promise (granule/async-get-metadata
component search-endpoint user-token params)
coll-promise (collection/async-get-metadata
search-endpoint user-token params)
errs (errors/collect params valid-lat valid-lon)]
(log/debug "Params: " params)
(log/debug "Bounding box: " bounding-box)
(log/debug "Finishing stage 1 ...")
[params bounding-box grans-promise coll-promise errs])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn stage2
[search-endpoint user-token params coll-promise grans-promise]
(log/debug "Starting stage 2 ...")
(let [granules (granule/extract-metadata grans-promise)
coll (collection/extract-metadata coll-promise)
data-files (map granule/extract-datafile-link granules)
service-ids (collection/extract-service-ids coll)
params (apply-level-conditions coll params)
vars (apply-bounding-conditions search-endpoint user-token coll params)
errs (apply errors/collect (concat [granules coll vars] data-files))]
(when errs
(log/error "Stage 2 errors:" errs))
(log/trace "data-files:" (vec data-files))
(log/trace "service ids:" service-ids)
(log/debug "Finishing stage 2 ...")
;; XXX coll is returned here because it's needed in a workaround
;; for different data sets using different starting points
;; for their indices in OPeNDAP
;;
;; XXX This is being tracked in CMR-4982
[coll params data-files service-ids vars errs])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn stage3
[coll search-endpoint user-token bounding-box service-ids vars]
;; XXX coll is required as an arg here because it's needed in a
;; workaround for different data sets using different starting
;; points for their indices in OPeNDAP
;;
;; XXX This is being tracked in CMR-4982
(log/debug "Starting stage 3 ...")
(let [services-promise (service/async-get-metadata
search-endpoint user-token service-ids)
bounding-infos (map #(variable/extract-bounding-info
coll % bounding-box)
vars)
errs (apply errors/collect bounding-infos)]
(when errs
(log/error "Stage 3 errors:" errs))
(log/trace "variables bounding-info:" (vec bounding-infos))
(log/debug "Finishing stage 3 ...")
[services-promise bounding-infos errs])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn stage4
[coll bounding-box services-promise bounding-infos]
(log/debug "Starting stage 4 ...")
(let [services (service/extract-metadata services-promise)
query (bounding-infos->opendap-query coll bounding-infos bounding-box)
errs (errors/collect services)]
(when errs
(log/error "Stage 4 errors:" errs))
(log/debug "services:" services)
(log/debug "Generated OPeNDAP query:" query)
(log/debug "Finishing stage 4 ...")
[query errs])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-opendap-urls
[component user-token raw-params]
(log/trace "Got params:" raw-params)
(let [start (util/now)
search-endpoint (config/get-search-url component)
;; Stage 1
[params bounding-box granules coll s1-errs] (stage1
component
search-endpoint
user-token
raw-params)
;; Stage 2
[coll params data-files service-ids vars s2-errs] (stage2
search-endpoint
user-token
params
coll
granules)
;; Stage 3
[services bounding-info s3-errs] (stage3
coll
search-endpoint
user-token
bounding-box
service-ids
vars)
;; Stage 4
[query s4-errs] (stage4 coll
bounding-box
services
bounding-info)
;; Error handling for all stages
errs (errors/collect
start params bounding-box granules coll s1-errs
data-files service-ids vars s2-errs
services bounding-info s3-errs
query s4-errs
{:errors (errors/check
[not data-files errors/empty-gnl-data-files])})]
(log/debug "Got data-files:" (vec data-files))
(if errs
(do
(log/error errs)
errs)
(let [urls-or-errs (data-files->opendap-urls params
data-files
query)]
;; Error handling for post-stages processing
(if (errors/erred? urls-or-errs)
urls-or-errs
(results/create urls-or-errs :elapsed (util/timed start))))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.variable (:require [clojure.string :as string] [cmr.opendap.components.config :as config] [cmr.opendap.const :as const] [cmr.opendap.errors :as errors] [cmr.opendap.http.request :as request] [cmr.opendap.http.response :as response] [cmr.opendap.ous.query.results :as results] [cmr.opendap.ous.util.geog :as geog] [cmr.opendap.util :as util] [ring.util.codec :as codec] [taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Notes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Notes on representing spatial extents. EDSC uses URL-encoded long/lat numbers representing a bounding box
Note that the ordering is the same as that used by CMR (see below).
OPeNDAP download URLs have something I haven't figured out yet; given that
one of the numbers if over 180, it can't be degrees ... it might be what
WCS uses for The OUS Prototype uses the WCS standard for lat/long:
CMR supports bounding spatial extents by describing a rectangle using four
comma-separated values:
1. lower left longitude
2. lower left latitude
3. upper right longitude
4. upper right latitude
For example:
Google's APIs use lower left, upper right, but the specify lat first, then
long:
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Constants/Default Values ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def default-dim-stride 1) (def default-lat-lon-stride 1) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
XXX The following two definitions are a hard-coded work-around for the fact that we don't currently have a mechanism for identifying the "direction of storage" or "endianness" of latitude data in different data sets: some store data from -90 to 90N starting at index 0, some from 90 to -90. XXX This is being tracked in CMR-4982 | (def lat-reversed-datasets
#{"Aqua AIRS Level 3 Daily Standard Physical Retrieval (AIRS+AMSU) V006 (AIRX3STD) at GES DISC"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn lat-reversed? [coll] ;; XXX coll is required as an arg here because it's needed in a ;; workaround for different data sets using different starting ;; points for their indices in OPeNDAP ;; ;; Ideally, we'll have something in a UMM-Var's metadata that ;; will allow us to make the reversed? assessment. ;; ;; XXX This is being tracked in CMR-4982 and CMR-4896 (contains? lat-reversed-datasets (:dataset_id coll))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Records ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; We're going to codify parameters with records to keep things well documented. Additionally, this will make converting between parameter schemes an explicit operation on explicit data. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord Point [lon lat]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord ArrayLookup [low high]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord BoundingInfo [;; :meta :concept-id concept-id ;; :umm :Name name ;; :umm :Dimensions, converted to EDN dimensions ;; Bounding box data from query params bounds ;; OPeNDAP lookup array opendap ;; :umm :Characteristics :Size size]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This lookup is needed for when latitude -90N is stored at the 0th index and 90N is stored at the highest index (whose actual number will varry, depending upon the resolution of the data). | (defn create-opendap-lookup
[lon-lo lat-lo lon-hi lat-hi]
(map->ArrayLookup
{:low {:lon lon-lo
:lat lat-lo}
:high {:lon lon-hi
:lat lat-hi}})) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This lookup is needed for when latitude 90N is stored at the 0th index and -90N is stored at the highest index (whose actual number will varry, depending upon the resolution of the data). | (defn create-opendap-lookup-reversed
[lon-lo lat-lo lon-hi lat-hi]
(let [lookup (create-opendap-lookup lon-lo lat-lo lon-hi lat-hi)
reversed-hi-lat (get-in lookup [:high :lat])
reversed-lo-lat (get-in lookup [:low :lat])]
(-> lookup
(assoc-in [:low :lat] reversed-hi-lat)
(assoc-in [:high :lat] reversed-lo-lat)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Support/Utility Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn normalize-lat-lon
[dim]
(-> dim
(assoc :Latitude (or (:Latitude dim)
(:lat dim)
;; XXX See CMR-4985
(:YDim dim))
:Longitude (or (:Longitude dim)
(:lon dim)
;; XXX See CMR-4985
(:XDim dim)))
(dissoc :lat :lon :XDim :YDim))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Core Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn build-query
[variable-ids]
(string/join
"&"
(conj
(map #(str (codec/url-encode "concept_id[]")
"=" %)
variable-ids)
(str "page_size=" (count variable-ids))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Given a 'params' data structure with a ':variables' key (which may or may not have values) and a list of all collection variable-ids, return the metadata for the passed variables, if defined, and for all associated variables, if params does not contain any. | (defn async-get-metadata
[search-endpoint user-token {variable-ids :variables}]
(if (seq variable-ids)
(let [url (str search-endpoint "/variables")
payload (build-query variable-ids)]
(log/debug "Variables query CMR URL:" url)
(log/debug "Variables query CMR payload:" payload)
(request/async-post
url
(-> {}
(request/add-token-header user-token)
(request/add-accept "application/vnd.nasa.cmr.umm+json")
(request/add-form-ct)
(request/add-payload payload))
response/json-handler))
(deliver (promise) []))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-metadata
[promise]
(let [rslts @promise]
(if (errors/erred? rslts)
(do
(log/error errors/variable-metadata)
rslts)
(do
(log/debug "Got results from CMR variable search:"
(results/elided rslts))
(log/trace "Remaining results:" (results/remaining-items rslts))
(:items rslts))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-metadata
[search-endpoint user-token variables]
(let [promise (async-get-metadata search-endpoint user-token variables)]
(extract-metadata promise))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn parse-lat-lon
[dim]
[(or (:Size (first (filter #(= "Longitude" (:Name %)) dim)))
(:Size (first (filter #(= "XDim" (:Name %)) dim)))
const/default-lon-abs-hi)
(or (:Size (first (filter #(= "Latitude" (:Name %)) dim)))
(:Size (first (filter #(= "YDim" (:Name %)) dim)))
const/default-lat-abs-hi)]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-dimensions
[entry]
(->> (get-in entry [:umm :Dimensions])
(map #(vector (keyword (:Name %)) (:Size %)))
(into (array-map)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Parse bounds that are annotated with Lat and Lon, returning values in the same order that CMR uses for spatial bounding boxes. | (defn parse-annotated-bounds
[bounds]
(let [lon-regex "Lon:\\s*(-?[0-9]+),\\s*(-?[0-9]+).*;\\s*"
lat-regex "Lat:\\s*(-[0-9]+),\\s*(-?[0-9]+).*"
[lon-lo lon-hi lat-lo lat-hi]
(rest (re-find (re-pattern (str lon-regex lat-regex)) bounds))]
[lon-lo lat-lo lon-hi lat-hi])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn parse-cmr-bounds [bounds] "Parse a list of lat/lon values ordered according to the CMR convention of lower-left lon, lower-left lat, upper-right long, upper-right lat." (map string/trim (string/split bounds #",\s*"))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn parse-bounds
[bounds]
(if (string/starts-with? bounds "Lon")
(parse-annotated-bounds bounds)
(parse-cmr-bounds bounds))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-bounds
[entry]
(when entry
(->> entry
(#(get-in % [:umm :Characteristics :Bounds]))
parse-bounds
(map #(Float/parseFloat %))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-opendap-bounds
([bounding-box]
(create-opendap-bounds bounding-box {:reversed? true}))
([bounding-box opts]
(create-opendap-bounds {:Longitude const/default-lon-abs-hi
:Latitude const/default-lat-abs-hi}
bounding-box
opts))
([{lon-max :Longitude lat-max :Latitude :as dimensions}
[lon-lo lat-lo lon-hi lat-hi :as bounding-box]
opts]
(log/trace "Got dimensions:" dimensions)
(when bounding-box
(let [lon-lo (geog/lon-lo-phase-shift lon-max lon-lo)
lon-hi (geog/lon-hi-phase-shift lon-max lon-hi)]
(if (:reversed? opts)
(let [lat-lo (geog/lat-lo-phase-shift-reversed lat-max lat-lo)
lat-hi (geog/lat-hi-phase-shift-reversed lat-max lat-hi)]
(create-opendap-lookup-reversed lon-lo lat-lo lon-hi lat-hi))
(let [lat-lo (geog/lat-lo-phase-shift lat-max lat-lo)
lat-hi (geog/lat-hi-phase-shift lat-max lat-hi)]
(create-opendap-lookup lon-lo lat-lo lon-hi lat-hi))))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn format-opendap-dim [min stride max] (format "[%s:%s:%s]" min stride max)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn format-opendap-dim-lat
([bounding-info]
(format-opendap-dim-lat bounding-info default-lat-lon-stride))
([bounding-info stride]
(if-let [opendap-bounds (:opendap bounding-info)]
(format-opendap-dim (get-in opendap-bounds [:low :lat])
stride
(get-in opendap-bounds [:high :lat]))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn format-opendap-dim-lon
([bounding-info]
(format-opendap-dim-lon bounding-info default-lat-lon-stride))
([bounding-info stride]
(if-let [opendap-bounds (:opendap bounding-info)]
(format-opendap-dim (get-in opendap-bounds [:low :lon])
stride
(get-in opendap-bounds [:high :lon]))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn replace-defaults-lat-lon
[bounding-info stride [k v]]
(cond (= k :Longitude) (format-opendap-dim-lon bounding-info stride)
(= k :Latitude) (format-opendap-dim-lat bounding-info stride)
:else (format-opendap-dim 0 stride (dec v)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn format-opendap-dims
([bounding-info]
(format-opendap-dims bounding-info default-dim-stride))
([bounding-info stride]
(if (:opendap bounding-info)
(->> bounding-info
:dimensions
(map (partial replace-defaults-lat-lon bounding-info stride))
(apply str))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-lat-lon-format-str
[bounding-info]
(log/debug "Original dimensions:" (:original-dimensions bounding-info))
(str (cond (get-in bounding-info [:original-dimensions :Latitude])
"Latitude"
(get-in bounding-info [:original-dimensions :lat])
"lat"
;; XXX See CMR-4985
(get-in bounding-info [:original-dimensions :YDim])
"Latitude")
"%s,"
(cond (get-in bounding-info [:original-dimensions :Longitude])
"Longitude"
(get-in bounding-info [:original-dimensions :lon])
"lon"
;; XXX See CMR-4985
(get-in bounding-info [:original-dimensions :XDim])
"Longitude")
"%s")) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn format-opendap-lat-lon
([bounding-info]
(format-opendap-lat-lon bounding-info default-lat-lon-stride))
([bounding-info stride]
(format (get-lat-lon-format-str bounding-info)
(format-opendap-dim-lat bounding-info stride)
(format-opendap-dim-lon bounding-info stride)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn format-opendap-bounds
([bounding-info]
(format-opendap-bounds bounding-info default-lat-lon-stride))
([{bound-name :name :as bounding-info} stride]
(log/trace "Bounding info:" bounding-info)
(format "%s%s"
bound-name
(format-opendap-dims bounding-info stride)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This function is executed at the variable level, however it has general, non-variable-specific bounding info passed to it in order to support spatial subsetting | (defn extract-bounding-info
[coll entry bounding-box]
;; XXX coll is required as an arg here because it's needed in a
;; workaround for different data sets using different starting
;; points for their indices in OPeNDAP
;;
;; XXX This is being tracked in CMR-4982
(log/trace "Got collection:" coll)
(log/trace "Got variable entry:" entry)
(log/trace "Got bounding-box:" bounding-box)
(if (:umm entry)
(let [original-dims (extract-dimensions entry)
dims (normalize-lat-lon original-dims)
;; XXX Once we sort out how to definitely extract lat/lon and
;; whether there is ever a need to go to
;; :umm :Characteristics :Bounds when we can just go to
;; :umm :Point instead, we can come back to this code
;; and remove the following line or integrate it into the
;; code.
;; XXX This is being tracked as part of CMR-4922 and CMR-4958
; bounds (or bounding-box (extract-bounds entry))
]
(map->BoundingInfo
{:concept-id (get-in entry [:meta :concept-id])
:name (get-in entry [:umm :Name])
:original-dimensions original-dims
:dimensions dims
:bounds bounding-box
:opendap (create-opendap-bounds
dims bounding-box {:reversed? (lat-reversed? coll)})
:size (get-in entry [:umm :Characteristics :Size])}))
{:errors [errors/variable-metadata]})) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.service (:require [clojure.string :as string] [cmr.opendap.components.config :as config] [cmr.opendap.errors :as errors] [cmr.opendap.http.request :as request] [cmr.opendap.http.response :as response] [cmr.opendap.ous.query.results :as results] [ring.util.codec :as codec] [taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn build-query
[service-ids]
(string/join
"&"
(conj
(map #(str (codec/url-encode "concept_id[]")
"=" %)
service-ids)
(str "page_size=" (count service-ids))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Given a service-id, get the metadata for the associate service. | (defn async-get-metadata
[search-endpoint user-token service-ids]
(if (seq service-ids)
(let [url (str search-endpoint "/services")
payload (build-query service-ids)]
(log/debug "Getting service metadata for:" service-ids)
(log/debug "Services query CMR URL:" url)
(log/debug "Services query CMR payload:" payload)
(request/async-post
url
(-> {}
(request/add-token-header user-token)
(request/add-accept "application/vnd.nasa.cmr.umm+json")
(request/add-form-ct)
(request/add-payload payload))
response/json-handler))
(deliver (promise) []))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn extract-metadata
[promise]
(let [rslts @promise]
(if (errors/erred? rslts)
(do
(log/error errors/service-metadata)
rslts)
(do
(log/debug "Got results from CMR service search:"
(results/elided rslts))
(log/trace "Remaining results:" (results/remaining-items rslts))
(:items rslts))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-metadata
[search-endpoint user-token service-ids]
(let [promise (async-get-metadata search-endpoint user-token service-ids)]
(extract-metadata promise))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn match-opendap [service-data] (= "opendap" (string/lower-case (:Type service-data)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.query.params.v1 (:require [clojure.set :as set] [cmr.opendap.ous.query.params.const :as const] [cmr.opendap.ous.util.core :as util])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord OusPrototypeParams [;; `format` is any of the formats supported by the target OPeNDAP server, ;; such as `json`, `ascii`, `nc`, `nc4`, `dods`, etc. format ;; ;; `coverage` can be: ;; * a list of granule concept ids ;; * a list of granule ccontept ids + a collection concept id ;; * a single collection concept id coverage ;; ;; `rangesubset` is a list of UMM-Var concept ids rangesubset ;; ;; `subset` is used to indicate desired spatial subsetting and is a list of ;; lon/lat values, as used in WCS. It is parsed from URL queries like so: ;; `?subset=lat(22,34)&subset=lon(169,200)` ;; giving values like so: ;; `["lat(22,34)" "lon(169,200)"]` subset ;; `timeposition` is used to indicate temporal subsetting with starting ;; and ending values being ISO 8601 datetime stamps, separated by a comma. timeposition]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def params-keys
(set/difference
(set (keys (map->OusPrototypeParams {})))
const/shared-keys)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn params?
[params]
(seq (set/intersection
(set (keys params))
params-keys))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-params
[params]
(map->OusPrototypeParams
(assoc params :format (or (:format params)
const/default-format)
:coverage (util/split-comma->sorted-coll (:coverage params))
:rangesubset (util/split-comma->sorted-coll (:rangesubset params))
:timeposition (util/->coll (:timeposition params))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.query.params.v2 (:require [clojure.set :as set] [cmr.opendap.ous.query.params.const :as const] [cmr.opendap.ous.util.core :as ous-util] [cmr.opendap.util :as util] [taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord CollectionParams [;; `collection-id` is the concept id for the collection in question. Note ;; that the collection concept id is not provided in query params, ;; but in the path as part of the REST URL. Regardless, we offer it here as ;; a record field. collection-id ;; ;; `format` is any of the formats supported by the target OPeNDAP server, ;; such as `json`, `ascii`, `nc`, `nc4`, `dods`, etc. format ;; ;; `granules` is list of granule concept ids; default behaviour is a ;; whitelist. granules ;; ;; `exclude-granules` is a boolean when set to true causes granules list ;; to be a blacklist. exclude-granules ;; ;; `variables` is a list of variables to be speficied when creating the ;; OPeNDAP URL. This is used for subsetting. variables ;; ;; `subset` is used the same way as `subset` for WCS where latitudes, ;; lower then upper, are given together and then longitude (again, lower ;; then upper) are given together. For instance, to indicate desired ;; spatial subsetting in URL queries: ;; `?subset=lat(56.109375,67.640625)&subset=lon(-9.984375,19.828125)` subset ;; ;; `bounding-box` is provided for CMR/EDSC-compatibility as an alternative ;; to using `subset` for spatial-subsetting. This parameter describes a ;; rectangular area of interest using four comma-separated values: ;; 1. lower left longitude ;; 2. lower left latitude ;; 3. upper right longitude ;; 4. upper right latitude ;; For example: ;; `bounding_box==-9.984375,56.109375,19.828125,67.640625` bounding-box ;; `temporal` is used to indicate temporal subsetting with starting ;; and ending values being ISO 8601 datetime stamps. temporal]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def params-keys
(set/difference
(set (keys (map->CollectionParams {})))
const/shared-keys)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn params?
[params]
(seq (set/intersection
(set (keys params))
params-keys))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn not-array?
[array]
(or (nil? array)
(empty? array))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-params
[params]
(let [bounding-box (ous-util/split-comma->coll (:bounding-box params))
subset (:subset params)
granules-array (ous-util/split-comma->coll
(get params (keyword "granules[]")))
variables-array (ous-util/split-comma->coll
(get params (keyword "variables[]")))
temporal-array (ous-util/->coll
(get params (keyword "temporal[]")))]
(log/trace "original bounding-box:" (:bounding-box params))
(log/trace "bounding-box:" bounding-box)
(log/trace "subset:" subset)
(log/trace "granules-array:" granules-array)
(log/trace "variables-array:" variables-array)
(map->CollectionParams
(assoc params
:format (or (:format params) const/default-format)
:granules (if (not-array? granules-array)
(ous-util/split-comma->sorted-coll (:granules params))
granules-array)
:variables (if (not-array? variables-array)
(ous-util/split-comma->sorted-coll (:variables params))
variables-array)
:exclude-granules (util/bool (:exclude-granules params))
:subset (if (seq bounding-box)
(ous-util/bounding-box->subset bounding-box)
(:subset params))
:bounding-box (if (seq bounding-box)
(mapv #(Float/parseFloat %) bounding-box)
(when (seq subset)
(ous-util/subset->bounding-box subset)))
:temporal (if (not-array? temporal-array)
(ous-util/->coll (:temporal params))
temporal-array))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord CollectionsParams [;; This isn't defined for the OUS Prototype, since it didn't support ;; submitting multiple collections at a time. As such, there is no ;; prototype-oriented record for this. ;; ;; `collections` is a list of `CollectionParams` records. collections]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace defines records for the accepted URL query parameters or, if using HTTP POST, keys in a JSON payload. Additionall, functions for working with these parameters are defined here. | (ns cmr.opendap.ous.query.params.core (:require [clojure.string :as string] [cmr.opendap.errors :as errors] [cmr.opendap.ous.query.params.v1 :as v1] [cmr.opendap.ous.query.params.v2 :as v2] [cmr.opendap.ous.util.core :as util] [taoensso.timbre :as log]) (:refer-clojure :exclude [parse])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn params?
[type params]
(case type
:v1 (v1/params? params)
:v2 (v2/params? params))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-params
[type params]
(case type
:v1 (v1/create-params params)
:v2 (v2/create-params params))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn v1->v2
[params]
(let [subset (:subset params)]
(-> params
(assoc :collection-id (or (:collection-id params)
(util/coverage->collection (:coverage params)))
:granules (util/coverage->granules (:coverage params))
:variables (:rangesubset params)
;; There was never an analog in v1 for exclude-granules, so set
;; to false.
:exclude-granules false
:bounding-box (when (seq subset)
(util/subset->bounding-box subset))
:temporal (:timeposition params))
(dissoc :coverage :rangesubset :timeposition)
(v2/map->CollectionParams)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn parse
[raw-params]
(log/trace "Got params:" raw-params)
(let [params (util/normalize-params raw-params)]
(cond (params? :v2 params)
(do
(log/trace "Parameters are of type `collection` ...")
(create-params :v2 params))
(params? :v1 params)
(do
(log/trace "Parameters are of type `ous-prototype` ...")
(v1->v2
(create-params :v1 params)))
(:collection-id params)
(do
(log/trace "Found collection id; assuming `collection` ...")
(create-params :v2 params))
:else
{:errors [errors/invalid-parameter
(str "Parameters: " params)]}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.query.params.const) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def default-format "nc") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def shared-keys
#{:collection-id :format :subset}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.ous.query.results) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord CollectionResults [;; The number of results returned hits ;; Number of milleseconds elapsed from start to end of call took ;; The actual items in the result set items]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create
[results & {:keys [elapsed]}]
(map->CollectionResults
{;; Our 'hits' is simplistic for now; will change when we support
;; paging, etc.
:hits (count results)
:took elapsed
:items results})) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn elided
[results]
(when (seq results)
(assoc results :items [(first (:items results) )"..."]))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn remaining-items
[results]
(when (seq results)
(rest (:items results)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace represents the authorization API for CMR OPeNDAP. This is where the rest of the application goes when it needs to perform checks on roles or permissions for a given user and/or concept. Currently, this namespace is only used by the REST middleware that checks resources for authorization. | (ns cmr.opendap.auth.core (:require [cmr.opendap.auth.permissions :as permissions] [cmr.opendap.auth.roles :as roles] [cmr.opendap.auth.token :as token] [cmr.opendap.components.caching :as caching] [cmr.opendap.components.config :as config] [cmr.opendap.errors :as errors] [cmr.opendap.http.response :as response] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
A supporting function for | (defn check-roles
[system handler request route-roles user-token user-id]
(log/debug "Checking roles annotated in routes ...")
(let [lookup (roles/cached-admin system user-token user-id)
errors (:errors lookup)]
(if errors
(response/not-allowed errors/no-permissions errors)
(if (roles/admin? route-roles lookup)
(handler request)
(response/not-allowed errors/no-permissions))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
A supporting function for | (defn check-permissions
[system handler request route-permissions user-token user-id]
(let [concept-id (permissions/route-concept-id request)
lookup (permissions/cached-concept
system user-token user-id concept-id)
errors (:errors lookup)]
(log/debug "Checking permissions annotated in routes ...")
(if errors
(response/not-allowed errors/no-permissions errors)
(if (permissions/concept? route-permissions
lookup
concept-id)
(handler request)
(response/not-allowed errors/no-permissions))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
A supporting function for | (defn check-roles-permissions
[system handler request route-roles route-permissions]
(if-let [user-token (token/extract request)]
(let [user-lookup (token/->cached-user system user-token)
errors (:errors user-lookup)]
(log/debug "ECHO token provided; proceeding ...")
(log/trace "user-lookup:" user-lookup)
(if errors
(response/not-allowed errors/token-required errors)
(do
(log/trace "user-token: [REDACTED]")
(log/trace "user-id:" user-lookup)
(cond ;; XXX For now, there is only the admin role in the CMR, so
;; we'll just keep this specific to that for now. Later, if
;; more roles are used, we'll want to make this more
;; generic ...
route-roles
(check-roles
system handler request route-roles user-token user-lookup)
route-permissions
(check-permissions system
handler
request
route-permissions
user-token
user-lookup)))))
(do
(log/warn "ECHO token not provided for protected resource")
(response/not-allowed errors/token-required)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This is the primary function for this namespace, utilized directly by CMR OPeNDAP's authorization middleware. Given a request which contains route-specific authorization requirements and potentially a user token, it checks against these as well as the level of access require for any requested concepts. | (defn check-route-access
[system handler request]
;; Before performing any GETs/POSTs against CMR Access Control or ECHO,
;; let's make sure that's actually necessary, only doing it in the cases
;; where the route is annotated for roles/permissions.
(let [route-roles (roles/route-annotation request)
route-permissions (permissions/route-annotation request)]
(if (or route-roles route-permissions)
(do
(log/debug (str "Either roles or permissions were annotated in "
"routes; checking ACLs ..."))
(log/debug "route-roles:" route-roles)
(log/debug "route-permissions:" route-permissions)
(check-roles-permissions
system handler request route-roles route-permissions))
(do
(log/debug (str "Neither roles nor permissions were annotated in "
"the routes; skipping ACL check ..."))
(handler request))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Roles for CMR OPeNDAP are utilized in the application routes when it is necessary to limit access to resources based on the role of a user. Roles are included in the route definition along with the route's handler. For example: ``` [... ["my/route" { :get {:handler my-handlers/my-route :roles #{:admin}} :post ...}] ...] | (ns cmr.opendap.auth.roles (:require [clojure.set :as set] [cmr.opendap.auth.acls :as acls] [cmr.opendap.components.caching :as caching] [cmr.opendap.components.config :as config] [reitit.ring :as ring] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The canonical ingest management ACL definition. | (def management-acl :INGEST_MANAGEMENT_ACL) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The query formatter used when making a roles query to the CMR Access Control
API. Note that only the management ACL is currently supported, and that this
maps below to | (def echo-management-query
{:system_object (name management-acl)}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Generate a key to be used for caching role data. | (defn roles-key [token] (str "roles:" token)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cmr-acl->reitit-acl
[cmr-acl]
(log/trace "Got CMR ACL:" cmr-acl)
(if (seq (management-acl cmr-acl))
#{:admin}
#{})) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Extract any roles annotated in the route associated with the given request. | (defn route-annotation [request] (get-in (ring/get-match request) [:data :get :roles])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Query the CMR Access Control API to get the roles for the given token+user. | (defn admin
[base-url token user-id]
(let [result @(acls/check-access base-url
token
user-id
echo-management-query)
errors (:errors result)]
;; NOTE: Unlike other parts of CMR OPeNDAP, we throw here instead of
;; passing around an error message due to the fact that the
;; caching code has this function burried inside, as acallback.
(if errors
(throw (ex-info (first errors) result))
(do
(log/debug "Got permissions:" result)
(cmr-acl->reitit-acl result))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Look up the roles for token+user in the cache; if there is a miss, make the actual call for the lookup. | (defn cached-admin
[system token user-id]
(try
(caching/lookup system
(roles-key token)
#(admin (config/get-access-control-url system)
token
user-id))
(catch Exception e
{:errors (ex-data e)}))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check to see if the roles of a given token+user match the required roles for the route. | (defn admin? [route-roles cache-lookup] (log/debug "Roles required-set:" route-roles) (log/debug "Roles has-set:" cache-lookup) (seq (set/intersection cache-lookup route-roles))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The functions in this API are responsible for such things as making queries to CMR Access Control to get token-to-user mappings, extracting tokens from request headers, and defining caching keys and related tasks. | (ns cmr.opendap.auth.token (:require [clojure.data.xml :as xml] [cmr.opendap.components.caching :as caching] [cmr.opendap.components.config :as config] [cmr.opendap.const :as const] [cmr.opendap.http.request :as request] [cmr.opendap.http.response :as response] [taoensso.timbre :as log] [xml-in.core :as xml-in])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The path segment to the ECHO REST API resource that is queried in order to get user/token mappings. | (def token-info-resource "/tokens/get_token_info") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Generate a key to be used for caching token data. | (defn token-data-key [token] (str "token-data:" token)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Generate a key to be used for caching user-id data. | (defn user-id-key [token] (str "user-id:" token)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Extract the value of | (defn extract [request] (request/get-header request "echo-token")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn find-xml [xml-str in-keys] (log/trace "Got token XML data:" xml-str) (xml-in/find-first (xml/parse-str xml-str) in-keys)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Parse the XML that is returned when querying the CMR Access Control API for token info. | (defn parse-token-data [xml-str] (find-xml xml-str [:token_info])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Parse the XML that is returned when querying the CMR Access Control API for the token. | (defn parse-token [xml-str] (find-xml xml-str [:token_info :token])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Parse the XML that is returned when querying the CMR Access Control API for the username associated with the token. | (defn parse-username [xml-str] (find-xml xml-str [:token_info :user_name])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Query the CMR Access Control API for information assocated with the given token. | (defn get-token-info
[base-url token]
(let [url (str base-url token-info-resource)
data (str "id=" token)]
(log/trace "Making token-info query to ECHO REST:" url)
(request/async-post
url
(-> {:body data}
(request/add-token-header token)
(request/add-form-ct))
#(response/client-handler % parse-username)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Given a token, return the associated user name. | (defn ->user
[base-url token]
(let [result @(get-token-info base-url token)
errors (:errors result)]
(if errors
(throw (ex-info (first errors) result))
result))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Look up the user for a token in the cache; if there is a miss, make the actual call for the lookup. | (defn ->cached-user
[system token]
(try
(caching/lookup
system
(user-id-key token)
#(->user (config/get-echo-rest-url system) token))
(catch Exception e
(ex-data e)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Permissions for CMR OPeNDAP are utilized in the application routes when it is necessary to limit access to resources based on the specific capabilities granted to a user. Permissions are included in the route definition along with the route's handler. For example: ``` [... ["my/route" { :get {:handler my-handlers/my-route :permissions #{:read}} :post ...}] ...] | (ns cmr.opendap.auth.permissions (:require [clojure.set :as set] [cmr.opendap.auth.acls :as acls] [cmr.opendap.components.caching :as caching] [cmr.opendap.components.config :as config] [reitit.ring :as ring] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The query formatter used when making a concept permissions query to the CMR Access Control API. | (def echo-concept-query #(hash-map :concept_id %)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Generate a key to be used for caching permissions data. | (defn permissions-key [token concept-id] (format "permissions:%s:%s" token concept-id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Construct permissions | (defn reitit-acl-data
[concept-id annotation]
(when (and concept-id annotation)
{concept-id annotation})) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Convert a CMR ACL to an ACL that can be matched against permissions in the reitit routing library's data structure. There following conditions are handled:
| (defn cmr-acl->reitit-acl
[cmr-acl]
(log/trace "Got CMR ACL:" cmr-acl)
(cond (nil? cmr-acl)
#{}
(map? cmr-acl)
(->> cmr-acl
(map (fn [[k v]] [(keyword k) (set (map keyword v))]))
(into {}))
:else cmr-acl)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Given a request, return the concept id for which we are checking permissions. | (defn route-concept-id [request] (get-in request [:path-params :concept-id])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn- -route-annotation [request] (get-in (ring/get-match request) [:data :get :permissions])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Extract any permissions annotated in the route associated with the given request. | (defn route-annotation
[request]
(let [annotation (-route-annotation request)]
(log/debug "Permissions annotation:" annotation)
(when annotation
(reitit-acl-data
(route-concept-id request)
(cmr-acl->reitit-acl annotation))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Query the CMR Access Control API to get the permissions the given token+user have for the given concept. | (defn concept
[base-url token user-id concept-id]
(let [result @(acls/check-access base-url
token
user-id
(echo-concept-query concept-id))
errors (:errors result)]
(if errors
(throw (ex-info (first errors) result))
(do
(log/debug "Got permissions:" result)
(cmr-acl->reitit-acl result))))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Look up the permissions for a concept in the cache; if there is a miss, make the actual call for the lookup. | (defn cached-concept
[system token user-id concept-id]
(try
(caching/lookup system
(permissions-key token concept-id)
#(concept (config/get-access-control-url system)
token
user-id
concept-id))
(catch Exception e
(ex-data e)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check to see if the concept permissions of a given token+user match the required permissions for the route. | (defn concept?
[route-perms cache-lookup concept-id]
(let [id (keyword concept-id)
required (cmr-acl->reitit-acl route-perms)
required-set (id required)
has-set (id cache-lookup)]
(log/debug "cache-lookup:" cache-lookup)
(log/debug "Permissions required-set:" required-set)
(log/debug "Permissions has-set:" has-set)
(seq (set/intersection required-set has-set)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace is provided for common code needed by the roles and permissions namespaces. | (ns cmr.opendap.auth.acls (:require [cheshire.core :as json] [clojure.set :as set] [cmr.opendap.components.caching :as caching] [cmr.opendap.http.request :as request] [cmr.opendap.http.response :as response] [org.httpkit.client :as httpc] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The path segment to the CMR Access Control API resource that is queried in order to get user permissions. | (def permissions-resource "/permissions") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This function is responsible for making a call to the CMR Access Control permissions resource to check what has been granted for the given user. | (defn check-access
[base-url token user-id acl-query]
(let [url (str base-url permissions-resource)
req {:query-params (merge {:user_id user-id}
acl-query)}]
(request/async-get
url
(request/add-token-header req token)
response/json-handler))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.util) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn bool
[arg]
(if (contains? #{true :true "true" "TRUE" "t" "T" 1} arg)
true
false)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn remove-empty [coll] (remove #(or (nil? %) (empty? %)) coll)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Merge maps recursively. | (defn deep-merge
[& maps]
(if (every? #(or (map? %) (nil? %)) maps)
(apply merge-with deep-merge maps)
(last maps))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn now [] (/ (System/currentTimeMillis) 1000)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn timed [start] (float (- (now) start))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This identifies the most frequently occuring data in a collection and returns it. | (defn most-frequent
[data]
(->> data
frequencies
;; the 'frequencies' function puts data first; let's swap the order
(map (fn [[k v]] [v k]))
;; sort in reverse order to get the highest counts first
(sort (comp - compare))
;; just get the highest
first
;; the first element is the count, the second is the bounding data
second)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.validation (:require [cmr.opendap.errors :as errors])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Predicates ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn check-latitude
[[low high]]
(and (>= low -90)
(<= high 90))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn check-longitude
[[low high]]
(and (>= low -180)
(<= high 180))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Validators ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn validate
[data predicate error-msg]
(if (predicate data)
data
{:errors [error-msg]})) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn validate-latitude [data] (validate data check-latitude errors/invalid-lat-params)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn validate-longitude [data] (validate data check-longitude errors/invalid-lon-params)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.core (:require [clojusc.twig :as logger] [cmr.opendap.components.core :as components] [com.stuartsierra.component :as component] [trifl.java :as trifl]) (:gen-class)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(logger/set-level! '[cmr.opendap] :info logger/no-color-log-formatter) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn -main
[& args]
(let [system (components/init)]
(component/start system)
(trifl/add-shutdown-handler #(component/stop system)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.testing.util (:require [cheshire.core :as json] [clojure.java.io :as io] [clojure.string :as string]) (:import (clojure.lang Keyword))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn parse-response
[response]
(try
(let [data (json/parse-string (:body response) true)]
(cond
(not (nil? (:items data)))
(:items data)
:else data))
(catch Exception e
{:error {:msg "Couldn't parse body."
:body (:body response)}}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-json-payload
[data]
{:body (json/generate-string data)}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-json-stream-payload
[data]
{:body (io/input-stream
(byte-array
(map (comp byte int)
(json/generate-string data))))}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-env-token
[^Keyword deployment]
(System/getenv (format "CMR_%s_TOKEN"
(string/upper-case (name deployment))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def get-sit-token #(get-env-token :sit)) (def get-uat-token #(get-env-token :uat)) (def get-prod-token #(get-env-token :prod)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.testing.system
(:require
[clojusc.dev.system.core :as system-api]
[clojusc.twig :as logger]
[cmr.opendap.components.config :as config]
[cmr.opendap.components.testing.system])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Setup and Constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Hide logging as much as possible before the system starts up, which should disable logging entirely for tests. | (logger/set-level! '[] :fatal) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def system-ns "cmr.opendap.components.testing.system") (def ^:dynamic *mgr* (atom nil)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
System API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn startup [] (alter-var-root #'*mgr* (constantly (atom (system-api/create-state-manager)))) (system-api/set-system-ns (:state @*mgr*) system-ns) (system-api/startup @*mgr*)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn shutdown
[]
(when *mgr*
(let [result (system-api/shutdown @*mgr*)]
(alter-var-root #'*mgr* (constantly (atom nil)))
result))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Convenience Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn system [] (system-api/get-system (:state @*mgr*))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-port [] (config/http-port (system))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Test Fixtures ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Testing fixture for system and integration tests. | (defn with-system [test-fn] (startup) (test-fn) (shutdown)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.testing.config
(:require
[clojusc.dev.system.core :as system-api]
[clojusc.twig :as logger]
[cmr.opendap.components.config :as config]
[cmr.opendap.components.testing.config])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Setup and Constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Hide logging as much as possible before the system starts up, which should disable logging entirely for tests. | (logger/set-level! '[] :fatal) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def system-ns "cmr.opendap.components.testing.config") (def ^:dynamic *mgr* (atom nil)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
System API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn startup [] (alter-var-root #'*mgr* (constantly (atom (system-api/create-state-manager)))) (system-api/set-system-ns (:state @*mgr*) system-ns) (system-api/startup @*mgr*)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn shutdown
[]
(when *mgr*
(let [result (system-api/shutdown @*mgr*)]
(alter-var-root #'*mgr* (constantly (atom nil)))
result))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Convenience Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn system [] (system-api/get-system (:state @*mgr*))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Test Fixtures ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Testing fixture for simple system tests that only require access to the configuration component. | (defn with-system [test-fn] (startup) (test-fn) (shutdown)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.components.caching
(:require
[clojure.core.cache :as cache]
[clojure.java.io :as io]
[com.stuartsierra.component :as component]
[cmr.opendap.components.config :as config]
[taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Support/utility Data & Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn load-cache
[system]
(if-let [sys system]
(if-let [filename (config/cache-dumpfile system)]
(try
(read-string
(slurp filename))
(catch Exception _ nil))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn dump-cache
[system cache-data]
(let [dumpfile (config/cache-dumpfile system)]
(io/make-parents dumpfile)
(spit
dumpfile
(prn-str cache-data)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Caching Component API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-cache
([system]
(create-cache system
(merge (config/cache-init system)
(load-cache system))))
([system init-items]
(let [ttl (config/cache-ttl-ms system)
threshold (config/cache-lru-threshold system)
cache (-> init-items
(cache/ttl-cache-factory :ttl ttl)
(cache/lru-cache-factory :threshold threshold))]
(log/debug "Creating TTL Cache with time-to-live of" ttl)
(log/debug "Composing with LRU cache with threshold (item count)" threshold)
(log/trace "Starting value:" init-items)
cache))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-cache [system] (get-in system [:caching :cache])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn evict [system item-key] (swap! (get-cache system) cache/evict item-key)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn evict-all [system] (reset! (get-cache system) (create-cache system (config/cache-init system)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn lookup
([system item-key]
(cache/lookup @(get-cache system) item-key))
([system item-key value-fn]
(let [ch @(get-cache system)]
(if (cache/has? ch item-key)
(do
(log/debug "Cache has key; skipping value function ...")
(log/trace "Key:" item-key)
(cache/hit ch item-key))
(when-let [value (value-fn)]
(log/debug "Cache miss; calling value function ...")
(when-not (or (nil? value) (empty? value))
(swap! (get-cache system) #(cache/miss % item-key value))))))
(lookup system item-key))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn lookup-all [system] @(get-cache system)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Lifecycle Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord Caching [cache]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn start
[this]
(log/info "Starting caching component ...")
(let [cache (atom (create-cache this))]
(log/debug "Started caching component.")
(assoc this :cache cache))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn stop
[this]
(log/info "Stopping caching component ...")
(if-let [cache-ref (:cache this)]
(if-let [cache @cache-ref]
(dump-cache this cache)))
(log/debug "Stopped caching component.")
(assoc this :cache nil)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def lifecycle-behaviour
{:start start
:stop stop}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(extend Caching component/Lifecycle lifecycle-behaviour) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Constructor ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-component
[]
(map->Caching {})) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.components.core
(:require
[cmr.opendap.components.caching :as caching]
[cmr.opendap.components.config :as config]
[cmr.opendap.components.httpd :as httpd]
[cmr.opendap.components.logging :as logging]
[com.stuartsierra.component :as component])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Common Configuration Components ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def cfg
{:config (config/create-component)}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def log
{:logging (component/using
(logging/create-component)
[:config])}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def cache
{:caching (component/using
(caching/create-component)
[:config :logging])}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def httpd
{:httpd (component/using
(httpd/create-component)
[:config :logging :caching])}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def cache-without-logging
{:caching (component/using
(caching/create-component)
[:config])}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def httpd-without-logging
{:httpd (component/using
(httpd/create-component)
[:config :caching])}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Initializations ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn initialize-config-only [] (component/map->SystemMap cfg)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn initialize-bare-bones
[]
(component/map->SystemMap
(merge cfg
log))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn initialize-with-web
[]
(component/map->SystemMap
(merge cfg
log
cache
httpd))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn initialize-without-logging
[]
(component/map->SystemMap
(merge cfg
cache-without-logging
httpd-without-logging))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def init-lookup
{:basic #'initialize-bare-bones
:testing-config-only #'initialize-config-only
:testing #'initialize-without-logging
:web #'initialize-with-web}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn init
([]
(init :web))
([mode]
((mode init-lookup)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.components.logging
(:require
[clojusc.twig :as logger]
[com.stuartsierra.component :as component]
[cmr.opendap.components.config :as config]
[taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Lifecycle Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord Logging []) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn start
[this]
(log/info "Starting logging component ...")
(let [log-level (config/log-level this)
log-nss (config/log-nss this)]
(log/debug "Setting up logging with level" log-level)
(log/debug "Logging namespaces:" log-nss)
(if (config/log-color? this)
(do
(log/debug "Enabling color logging ...")
(logger/set-level! log-nss log-level))
(logger/set-level! log-nss log-level logger/no-color-log-formatter))
(log/debug "Started logging component.")
this)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn stop [this] (log/info "Stopping logging component ...") (log/debug "Stopped logging component.") this) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def lifecycle-behaviour
{:start start
:stop stop}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(extend Logging component/Lifecycle lifecycle-behaviour) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Constructor ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-component
[]
(map->Logging {})) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
A component system setup namespace for use in testing. | (ns cmr.opendap.components.testing.system
(:require
[cmr.opendap.components.core :as core])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Initialization ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn init
([]
(init :testing))
([mode]
((mode core/init-lookup)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
A component system setup namespace for use in testing. | (ns cmr.opendap.components.testing.config
(:require
[cmr.opendap.components.core :as core])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Initialization ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn init
([]
(init :testing-config-only))
([mode]
((mode core/init-lookup)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.components.config (:require [cmr.opendap.config :as config] [com.stuartsierra.component :as component] [taoensso.timbre :as log]) (:import (clojure.lang Keyword))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Utility Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn- get-cfg
[system]
(->> [:config :data]
(get-in system)
(into {}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Config Component API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn api-version [system] (:api-version (get-cfg system))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn api-version-dotted [system] (str "." (api-version system))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn default-content-type [system] (:default-content-type (get-cfg system))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cache-dumpfile [system] (get-in (get-cfg system) [:caching :dumpfile])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cache-init [system] (get-in (get-cfg system) [:caching :init])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cache-lru-threshold [system] (get-in (get-cfg system) [:caching :lru :threshold])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cache-ttl-ms
[system]
(* (get-in (get-cfg system) [:caching :ttl :minutes]) ; minutes
60 ; seconds
1000 ; milliseconds)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cache-type [system] (get-in (get-cfg system) [:caching :type])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cmr-max-pagesize [system] (get-in (get-cfg system) [:cmr :max-pagesize])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-service
[system service]
(let [svc-cfg (get-in (get-cfg system)
(concat [:cmr] (config/service-keys service)))]
svc-cfg)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cmr-base-url [system] (config/service->base-url (get-service system :search))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This function returns the cmr-opendap URL with a trailing slash, but without the 'opendap' appended. | (defn opendap-base-url [system] (str (config/service->base-public-url (get-service system :opendap)) "/")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This function returns the cmr-opendap URL with a trailing slash. | (defn opendap-url [system] (str (config/service->public-url (get-service system :opendap)) "/")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-service-url [system service] (config/service->url (get-service system service))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The URLs returned by these functions have no trailing slash: | (def get-access-control-url #(get-service-url % :access-control)) (def get-echo-rest-url #(get-service-url % :echo-rest)) (def get-ingest-url #(get-service-url % :ingest)) (def get-opendap-url #(get-service-url % :opendap)) (def get-search-url #(get-service-url % :search)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-assets [system] (get-in (get-cfg system) [:httpd :assets])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-docs [system] (get-in (get-cfg system) [:httpd :docs])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-port
[system]
(or (get-in (get-cfg system) [:cmr :opendap :port])
(get-in (get-cfg system) [:httpd :port]))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-index-dirs [system] (get-in (get-cfg system) [:httpd :index-dirs])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-replace-base-url [system] (get-in (get-cfg system) [:httpd :replace-base-url])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-rest-docs-base-url-template [system] (get-in (get-cfg system) [:httpd :rest-docs :base-url-template])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-rest-docs-outdir [system] (get-in (get-cfg system) [:httpd :rest-docs :outdir])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-rest-docs-source [system] (get-in (get-cfg system) [:httpd :rest-docs :source])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn http-skip-static [system] (get-in (get-cfg system) [:httpd :skip-static])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn log-color?
[system]
(or (get-in (get-cfg system) [:cmr :opendap :logging :color])
(get-in (get-cfg system) [:logging :color]))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn log-level [system] (get-in (get-cfg system) [:logging :level])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn log-nss [system] (get-in (get-cfg system) [:logging :nss])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn streaming-heartbeat [system] (get-in (get-cfg system) [:streaming :heartbeat])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn streaming-timeout [system] (get-in (get-cfg system) [:streaming :timeout])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn vendor [system] (:vendor (get-cfg system))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Lifecycle Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord Config [data]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn start
[this]
(log/info "Starting config component ...")
(log/debug "Started config component.")
(let [cfg (config/data)]
(log/debug "Built configuration:" cfg)
(assoc this :data cfg))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn stop [this] (log/info "Stopping config component ...") (log/debug "Stopped config component.") this) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def lifecycle-behaviour
{:start start
:stop stop}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(extend Config component/Lifecycle lifecycle-behaviour) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Constructor ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-component
[]
(map->Config {})) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.components.httpd
(:require
[com.stuartsierra.component :as component]
[cmr.opendap.components.config :as config]
[cmr.opendap.app.core :as app]
[org.httpkit.server :as server]
[taoensso.timbre :as log])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Lifecycle Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defrecord HTTPD []) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn start
[this]
(log/info "Starting httpd component ...")
(let [port (config/http-port this)
server (server/run-server (app/main this) {:port port})]
(log/debugf "HTTPD is listening on port %s" port)
(log/debug "Started httpd component.")
(assoc this :server server))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn stop
[this]
(log/info "Stopping httpd component ...")
(if-let [server (:server this)]
(server))
(assoc this :server nil)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def lifecycle-behaviour
{:start start
:stop stop}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(extend HTTPD component/Lifecycle lifecycle-behaviour) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Component Constructor ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn create-component
[]
(map->HTTPD {})) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.errors (:require [clojure.set :as set])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Defaults ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def default-error-code 400) (def auth-error-code 403) (def client-error-code 400) (def server-error-code 500) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Error Messages ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Authorization | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def no-permissions "You do not have permissions to access that resource.") (def token-required "An ECHO token is required to access this resource.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Generic | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def status-code "HTTP Error status code: %s.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
OUS - General | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def not-implemented "This capability is not currently implemented.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def unsupported "This capability is not currently supported.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def unsupported-processing-level "The requst includes a dataset whose processing level is not supported.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def problem-processing-level "Problematic processing level %s for collection %s.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
OUS - Parameters | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def invalid-parameter "One or more of the parameters provided were invalid.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def invalid-lat-params
(str "The values provided for latitude are not within the valid range of "
"-90 degrees through 90 degrees.")) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def invalid-lon-params
(str "The values provided for longitude are not within the valid range of "
"-180 degrees through 180 degrees.")) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
OUS - CMR Metadata | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def problem-granules "Problematic granules: [%s].") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def empty-svc-pattern
(str "The service pattern computed was empty. Is there a service associated "
"with the given collection? Does the UMM-S record in question have "
"values for the pattern fields?")) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def empty-gnl-data-file-url
(str "There was a problem extracting a data URL from the granule's service "
"data file.")) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def empty-gnl-data-files "There was a problem extracting a service data file from the granule.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def no-matching-service-pattern
(str "There was a problem creating URLs from granule file data: couldn't "
"match default service pattern %s to service %s.")) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def granule-metadata "There was a problem extracting granule metadata.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def service-metadata "There was a problem extracting service metadata.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def variable-metadata "There was a problem extracting variable metadata.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
OUS - Results | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def empty-query-string "No OPeNDAP query string was generated for the request.") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This is a lookup data structure for how HTTP status/error codes map to CMR OPeNDAP errors. | (def status-map
{client-error-code #{empty-svc-pattern
invalid-lat-params
invalid-lon-params
not-implemented
unsupported
unsupported-processing-level
problem-processing-level}
auth-error-code #{no-permissions
token-required}
server-error-code #{empty-gnl-data-files
;;empty-gnl-data-file-url
problem-granules
no-matching-service-pattern
granule-metadata
service-metadata
variable-metadata}}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Error Handling API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn any-auth-errors?
[errors]
(seq (set/intersection (get status-map auth-error-code)
(set (:errors errors))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn any-client-errors?
[errors]
(seq (set/intersection (get status-map client-error-code)
(set (:errors errors))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn any-server-errors?
[errors]
(seq (set/intersection (get status-map server-error-code)
(set (:errors errors))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn check
[& msgs]
(remove nil? (map (fn [[check-fn value msg]] (when (check-fn value) msg))
msgs))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-errors
[data]
(or (:errors data)
(when-let [error (:error data)]
[error]))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn erred? [data] (seq (get-errors data))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn any-erred? [coll] (some erred? coll)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn collect
[& coll]
(let [errors (vec (remove nil? (mapcat get-errors coll)))]
(when (seq errors)
{:errors errors}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.http.request (:require [cmr.opendap.components.config :as config] [cmr.opendap.const :as const] [org.httpkit.client :as httpc] [taoensso.timbre :as log]) (:refer-clojure :exclude [get])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Header Support ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get-header [req field] (get-in req [:headers field])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn add-header
([field value]
(add-header {} field value))
([req field value]
(assoc-in req [:headers field] value))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn add-accept
([value]
(add-accept {} value))
([req value]
(add-header req "Accept" value))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn add-token-header
([token]
(add-token-header {} token))
([req token]
(add-header req "Echo-Token" token))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn add-user-agent
([]
(add-user-agent {}))
([req]
(add-header req "User-Agent" const/user-agent))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn add-content-type
([ct]
(add-content-type {}))
([req ct]
(add-header req "Content-Type" ct))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn add-form-ct
([]
(add-form-ct {}))
([req]
(add-content-type req "application/x-www-form-urlencoded"))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn add-client-id
([]
(add-client-id {}))
([req]
(add-header req "Client-Id" const/client-id))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn add-payload
([data]
(add-payload {} data))
([req data]
(assoc req :body data))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HTTP Client Support ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def default-options
{:user-agent const/user-agent
:insecure? true}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn options [req & opts] (apply assoc (concat [req] opts))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn request
[method url req & [callback]]
(httpc/request (-> default-options
(add-client-id)
(add-user-agent)
(merge req)
(assoc :url url :method method)
((fn [x] (log/trace "Options to httpc:" x) x)))
callback)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn async-get
([url]
(async-get url {}))
([url req]
(async-get url req nil))
([url req callback]
(request :get url req callback))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn async-post
([url]
(async-post url {:body nil}))
([url req]
(async-post url req nil))
([url req callback]
(request :post url req callback))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn get [& args] @(apply async-get args)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn post [& args] @(apply async-post args)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Accept Header/Version Support ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The regular expression for the The subtype is then further broken down into the following groups: * vendor * version (with and without the '.' * content-type (with and without the '+' as well as the case where no vendor is supplied)) All other groups are unused. | (def accept-pattern (re-pattern "(.+)/((vnd\\.([^.+]+)(\\.(v[0-9]+))?(\\+(.+))?)|(.+))")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def accept-pattern-keys [:all :type :subtype :vendor+version+content-type :vendor :.version :version :+content-type :content-type :no-vendor-content-type]) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn default-accept
[system]
(format "application/vnd.%s%s+%s"
(config/vendor system)
(config/api-version-dotted system)
(config/default-content-type system))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn parse-accept
[system req]
(->> (or (get-in req [:headers :accept])
(get-in req [:headers "accept"])
(get-in req [:headers "Accept"])
(default-accept system))
(re-find accept-pattern)
(zipmap accept-pattern-keys))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn accept-api-version
[system req]
(let [parsed (parse-accept system req)
version (or (:version parsed) (config/api-version system))]
version)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn accept-media-type
[system req]
(let [parsed (parse-accept system req)
vendor (or (:vendor parsed) (config/vendor system))
version (or (:.version parsed) (config/api-version-dotted system))]
(str vendor version))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn accept-format
[system req]
(let [parsed (parse-accept system req)]
(or (:content-type parsed)
(:no-vendor-content-type parsed)
(config/default-content-type system)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This namespace defines a default set of transform functions suitable for use in presenting results to HTTP clients. Note that ring-based middleeware may take advantage of these functions either by single use or composition. | (ns cmr.opendap.http.response (:require [cheshire.core :as json] [clojure.data.xml :as xml] [clojure.string :as string] [cmr.opendap.errors :as errors] [ring.util.http-response :as response] [taoensso.timbre :as log] [xml-in.core :as xml-in]) (:refer-clojure :exclude [error-handler])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn parse-json-body
[body]
(let [str-data (if (string? body) body (slurp body))
json-data (json/parse-string str-data true)]
(log/trace "str-data:" str-data)
(log/trace "json-data:" json-data)
json-data)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn json-errors [body] (:errors (parse-json-body body))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn parse-xml-body
[body]
(let [str-data (if (string? body) body (slurp body))]
(xml/parse-str str-data))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn xml-errors
[body]
(vec (xml-in/find-all (parse-xml-body body)
[:errors :error]))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn error-handler
[status headers body]
(let [default-msg (format errors/status-code status)
ct (:content-type headers)]
(log/trace "Headers:" headers)
(log/trace "Content-Type:" ct)
(log/trace "Body:" body)
(cond (nil? ct)
(do
(log/error body)
{:errors [body]})
(string/starts-with? ct "application/xml")
(let [errs (xml-errors body)]
(log/error errs)
{:errors errs})
(string/starts-with? ct "application/json")
(let [errs (json-errors body)]
(log/error errs)
{:errors errs})
:else
{:errors [default-msg]}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn client-handler
([response]
(client-handler response identity))
([{:keys [status headers body error]} parse-fn]
(log/debug "Handling client response ...")
(cond error
(do
(log/error error)
{:errors [error]})
(>= status 400)
(error-handler status headers body)
:else
(do
(log/trace "headers:" headers)
(log/trace "body:" body)
(parse-fn body))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def json-handler #(client-handler % parse-json-body)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn ok [_request & args] (response/ok args)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn process-ok-results
[data]
{:headers {"CMR-Took" (:took data)
"CMR-Hits" (:hits data)}
:status 200}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn process-err-results
[data]
(cond (errors/any-auth-errors? data)
{:status errors/auth-error-code}
(errors/any-server-errors? data)
{:status errors/server-error-code}
(errors/any-client-errors? data)
{:status errors/client-error-code}
:else
{:status errors/default-error-code})) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn process-results
[data]
(if (:errors data)
(process-err-results data)
(process-ok-results data))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn json
[_request data]
(-> data
process-results
(assoc :body (json/generate-string data))
(response/content-type "application/json"))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn text
[_request data]
(-> data
process-results
(assoc :body data)
(response/content-type "text/plain"))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn html
[_request data]
(-> data
process-results
(assoc :body data)
(response/content-type "text/html"))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn not-found [_request] (response/content-type (response/not-found "Not Found") "text/plain")) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cors
[request response]
(case (:request-method request)
:options (-> response
(response/content-type "text/plain; charset=utf-8")
(response/header "Access-Control-Allow-Origin" "*")
(response/header "Access-Control-Allow-Methods" "POST, PUT, GET, DELETE, OPTIONS")
(response/header "Access-Control-Allow-Headers" "Content-Type")
(response/header "Access-Control-Max-Age" "2592000"))
(response/header response "Access-Control-Allow-Origin" "*"))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn add-header [response field value] (assoc-in response [:headers (if (string? field) field (name field))] value)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn version-media-type [response value] (add-header response :cmr-media-type value)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn errors
[errors]
{:errors errors}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn error [error] (errors [error])) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn not-allowed
([message]
(not-allowed message []))
([message other-errors]
(-> (conj other-errors message)
errors
json/generate-string
response/forbidden
(response/content-type "application/json")))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.const) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
XXX We should move these to configuration; this would mean that anything that requires these values would need access to the 'config' component thus also requiring that the calling function has access to the system component ... | (def client-id "cmr-opendap-service") (def user-agent "CMR OPeNDAP Service/1.0 (+https://github.com/cmr-exchange/cmr-opendap)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
XXX The following is used as a criteria for extracing data files from granule metadata. This may change once CMR-4912 is addressed. | (def datafile-link-rel "http://esipfed.org/ns/fedsearch/1.1/data#") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def default-lon-lo -180.0) (def default-lon-hi 180.0) (def default-lat-lo -90.0) (def default-lat-hi 90.0) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def default-lon-abs-lo 0.0) (def default-lon-abs-hi 360.0) (def default-lat-abs-lo 0.0) (def default-lat-abs-hi 180.0) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def default-lat-lon-resolution 1) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The functions of this namespace are specifically responsible for generating data structures to be consumed by site page templates. Of special note: this namespace and its sibling Under no circumstances should | (ns cmr.opendap.site.data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Data Utility Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def default-title "CMR OPeNDAP") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Data for templates that display a link to Partner Guides. Clients should overrirde these keys in their own base static and base page maps if they need to use different values. | (def default-partner-guide
{:partner-url "https://wiki.earthdata.nasa.gov/display/CMR/CMR+Client+Partner+User+Guide"
:partner-text "Client Partner's Guide"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Data that all static pages have in common. Note that static pages don't have any context. | (defn base-static
[]
(merge default-partner-guide
{:base-url ""
:app-title default-title})) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Data that all pages have in common. Note that dynamic pages need to provide the base-url. | (defn base-dynamic
([]
(base-dynamic {}))
([data]
(merge default-partner-guide
{:app-title default-title}
data))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Page Data Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Data that all app pages have in common. The The default variant is the original, designed to work with the regular request context which contains the state of a running CMR. | (defmulti base-page :execution-context) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defmethod base-page :cli [data] (base-static data)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defmethod base-page :default [data] (base-dynamic data)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The functions of this namespace are specifically responsible for returning ready-to-serve pages. | (ns cmr.opendap.site.pages (:require [cmr.opendap.site.data :as data] [selmer.parser :as selmer] [ring.util.response :as response] [taoensso.timbre :as log])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Page Utility Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
A utility function for preparing templates. | (defn render-template [template page-data] (response/response (selmer/render-file template page-data))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
A utility function for preparing HTML templates. | (defn render-html [template page-data] (response/content-type (render-template template page-data) "text/html")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HTML page-genereating functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Prepare the home page template. | (defn home [request data] (render-html "templates/opendap-home.html" (data/base-dynamic data))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Prepare the top-level search docs page. | (defn opendap-docs [request data] (log/debug "Calling opendap-docs page ...") (render-html "templates/opendap-docs.html" (data/base-dynamic data))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Prepare the home page template. | (defn not-found
([request]
(not-found request {:base-url "/opendap"}))
([request data]
(render-html
"templates/opendap-not-found.html"
(data/base-dynamic data)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The functions of this namespace are specifically responsible for generating the static resources of the top-level and site pages and sitemaps. | (ns cmr.opendap.site.static (:require [clojure.java.io :as io] [clojusc.twig :as logger] [cmr.opendap.components.config :as config] [cmr.opendap.components.core :as components] [cmr.opendap.site.data :as data] [com.stuartsierra.component :as component] [markdown.core :as markdown] [selmer.parser :as selmer] [taoensso.timbre :as log] [trifl.java :as trifl]) (:gen-class)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(logger/set-level! '[cmr.opendap] :info) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Utility Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This is the function used by default to render templates, given data that the template needs to render. | (defn generate
[target template-file data]
(log/debug "Rendering data from template to:" target)
(log/debug "Template:" template-file)
(log/debug "Data:" data)
(io/make-parents target)
(->> data
(selmer/render-file template-file)
(spit target))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Content Generators ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Generate the HTML for the CMR OPeNDAP REST API docs page. | (defn generate-rest-api-docs
[docs-source docs-dir base-url]
(generate
(format "%s/index.html" docs-dir)
"templates/opendap-docs-static.html"
{:base-url base-url
:page-content (markdown/md-to-html-string (slurp docs-source))})) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
A convenience function that pulls together all the static content generators in this namespace. This is the function that should be called in the parent static generator namespace. | (defn generate-all [docs-source docs-dir base-url] (log/debug "Generating static site files ...")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn -main
[& args]
(let [system-init (components/init :basic)
system (component/start system-init)]
(trifl/add-shutdown-handler #(component/stop system))
(generate-all
(config/http-rest-docs-source system)
(config/http-rest-docs-outdir system)
(config/http-rest-docs-base-url-template system)))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.health) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn has-data?
[x]
(if (nil? x)
false
true)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn config-ok? [component] (has-data? (:config component))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn logging-ok? [component] (has-data? (:logging component))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn components-ok?
[component]
{:config {:ok? (config-ok? component)}
:httpd {:ok? true}
:logging {:ok? (logging-ok? component)}}) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(ns cmr.opendap.config
(:require
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as string]
[cmr.opendap.util :as util]
[environ.core :as environ])
(:import
(clojure.lang Keyword))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(def config-file "config/cmr-opendap/config.edn") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cfg-data
([]
(cfg-data config-file))
([filename]
(with-open [rdr (io/reader (io/resource filename))]
(edn/read (new java.io.PushbackReader rdr))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn cmr-only
[[k v]]
(let [key-name (name k)]
(when (string/starts-with? key-name "cmr-")
[(mapv keyword (string/split key-name #"-"))
(try
(Integer/parseInt v)
(catch Exception _e
v))]))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn nest-vars [acc [ks v]] (assoc-in acc ks v)) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn env-props-data
[]
(->> (#'environ/read-system-props)
(util/deep-merge (#'environ/read-system-env))
(map cmr-only)
(remove nil?)
(reduce nest-vars {}))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn data
[]
(util/deep-merge (cfg-data)
(env-props-data))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
We need to special-case two-word services, as split by the environment and system property parser above. | (defn service-keys
[^Keyword service]
(cond (or (= service :access)
(= service :access-control))
[:access :control]
(or (= service :echo)
(= service :echo-rest))
[:echo :rest]
:else [service])) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn service->base-url
[service]
(format "%s://%s:%s"
(or (:protocol service) "https")
(:host service)
(or (:port service) "443"))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn service->url
[service]
(format "%s%s"
(service->base-url service)
(or (get-in service [:relative :root :url])
(:context service)
"/"))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn service->base-public-url
[service]
(let [protocol (or (get-in service [:public :protocol]) "https")
host (get-in service [:public :host])]
(if (= "https" protocol)
(format "%s://%s" protocol host)
(format "%s://%s:%s" protocol host (get-in service [:public :port]))))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(defn service->public-url
[service]
(format "%s%s"
(service->base-public-url service)
(or (get-in service [:relative :root :url])
(:context service)
"/"))) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||