OPeNDAP/CMR Integration

1.0.0-SNAPSHOT


OPeNDAP Integration in the CMR

dependencies

cheshire
5.8.0
clojusc/trifl
0.2.0
clojusc/twig
0.3.2
com.stuartsierra/component
0.3.2
environ
1.1.0
gov.nasa.earthdata/cmr-authz
0.1.0-SNAPSHOT
gov.nasa.earthdata/cmr-http-kit
0.1.0-SNAPSHOT
gov.nasa.earthdata/cmr-site-templates
0.1.0-SNAPSHOT
http-kit
2.3.0
markdown-clj
1.0.2
metosin/reitit-core
0.1.1-SNAPSHOT
metosin/reitit-ring
0.1.1-SNAPSHOT
metosin/ring-http-response
0.9.0
org.clojure/clojure
1.9.0
org.clojure/core.async
0.4.474
org.clojure/core.cache
0.7.1
org.clojure/data.xml
0.2.0-alpha5
ring/ring-core
1.6.3
ring/ring-codec
1.1.0
ring/ring-defaults
0.3.1
selmer
1.11.7
tolitius/xml-in
0.1.0



(this space intentionally left almost blank)
 

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.authz.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.auth-cache
  (:require
   [clojure.java.io :as io]
   [clojusc.twig :as twig]
   [cmr.authz.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.components.auth :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.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.auth-cache :as auth-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 (auth-cache-handler/lookup-all httpd-component)
            :roles #{:admin}}
      :delete {:handler (auth-cache-handler/evict-all httpd-component)
               :roles #{:admin}}}]
     ["/opendap/cache/:item-key" {
      :get {:handler (auth-cache-handler/lookup httpd-component)
            :roles #{:admin}}
      :delete {:handler (auth-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/trace "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 cmr.opendap.ous.variable/create-opendap-lookup-reversed).

(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 cmr.opendap.ous.variable/create-opendap-lookup-reversed).

(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: [lower-longitude, lower-latitude, upper-longitude, upper-latitude].

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 fallback-* vars are left-overs from previous work done in the Node.js prorotype. For more context, see CMR-4901 abd CMR-4912. Their continued use is a special case that needs to be addressed before CMR OPeNDAP can be used in general, for all granules. As such. the work in CMR-4912 will need to be finished before we can remove/ update the following:

(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/trace "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/trace
              "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:

  • no spatial subsetting and no variables - return no query string in OPeNDAP URL; this will give users all variables for the entire extent defined in the variables' metadata.
  • variables but no spatial subsetting - return a query string with just the variables requested; a Latitude,Longitude will also be appended to the OPeNDAP URL; this will give users just these variables, but for the entire extent defined in each variable's metadata.
  • variables and spatial subsetting - return a query string with the variables requested as well as the subsetting requested; this will give users just these variables, with data limited to the specified spatial range.
  • spatial subsetting but no variables - this is a special case that needs to do a little more work: special subsetting without variables will link to an essentially empty OPeNDAP file; as such, we need to iterate through all the variables in the metadata and create an OPeNDAP URL query string that provides the sensible default of all variables.

    For each of those conditions, a different value of vars will be returned, allowing for the desired result. Respective to the bullet points above:

  • vars - empty vector

  • vars - metadata for all the specified variable ids
  • vars - metadata for all the specified variable ids
  • vars - metadata for all the variables associated in the collection
(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 let block in get-opendap-urls but now have been split out into stages organized by dependency.

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/trace "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/trace "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)
          (do
            (log/error urls-or-errs)
            urls-or-errs)
          (do
            (log/debug "Generated URLs:" (vec 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). -9.984375%2C56.109375%2C19.828125%2C67.640625 which URL-decodes to: -9.984375,56.109375,19.828125,67.640625

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 x and y? Latitude[22:34],Longitude[169:200]

The OUS Prototype uses the WCS standard for lat/long: SUBSET=axis[,crs](low,high) For lat/long this takes the following form: subset=lat(56.109375,67.640625)&subset=lon(-9.984375,19.828125)

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: bounding_box==-9.984375,56.109375,19.828125,67.640625

Google's APIs use lower left, upper right, but the specify lat first, then long: southWest = LatLng(56.109375,-9.984375); northEast = LatLng(67.640625,19.828125);

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"
    "MODIS/Terra Aerosol Cloud Water Vapor Ozone Daily L3 Global 1Deg CMG V006"})
(defn lat-reversed?
  [coll]
  (log/debug "Checking collection for reversed latitudinal values ...")
  (let [dataset-id (:dataset_id coll)]
    (log/debug "Data set id:" dataset-id)
    ;; 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)))

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/trace "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)]
           (log/debug "Variable latitudinal values are reversed ...")
           (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])
             "YDim")
       "%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])
             "XDim")
       "%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/trace "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))))
 
(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.core
  (:require
    [cmr.authz.components.caching :as auth-caching]
    [cmr.opendap.components.auth :as auth]
    [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 auth-cache
  {:auth-caching (component/using
                  (auth-caching/create-component)
                  [:config :logging])})
(def authz
  {:auth (component/using
          (auth/create-component)
          [:auth-caching])})
(def httpd
  {:httpd (component/using
           (httpd/create-component)
           [:config :logging :auth-caching :auth])})
(def auth-cache-without-logging
  {:auth-caching (component/using
                  (auth-caching/create-component)
                  [:config])})
(def httpd-without-logging
  {:httpd (component/using
           (httpd/create-component)
           [:config :auth-caching :auth])})

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
           auth-cache
           authz
           httpd)))
(defn initialize-without-logging
  []
  (component/map->SystemMap
    (merge cfg
           auth-cache-without-logging
           authz
           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))))
 

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.components.auth
  (:require
   [clojure.set :as set]
   [cmr.authz.components.caching :as caching]
   [cmr.authz.components.config :as config]
   [cmr.authz.errors :as errors]
   [cmr.authz.permissions :as permissions]
   [cmr.authz.roles :as roles]
   [cmr.authz.token :as token]
   [cmr.http.kit.response :as response]
   [com.stuartsierra.component :as component]
   [taoensso.timbre :as log]))

Support/utility Data & Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Check to see if the roles of a given token+user match the required roles for the route.

(defn admin-role?
  [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)))

Check to see if the concept permissions of a given token+user match the required permissions for the route.

(defn concept-permission?
  [route-perms cache-lookup concept-id]
  (let [id (keyword concept-id)
        required (permissions/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))))

Caching Component API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

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
     (token/user-id-key token)
     #(token/->user (config/get-echo-rest-url system) token))
    (catch Exception e
      (log/error e)
      (ex-data e))))

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-role
  [system token user-id]
  (try
    (caching/lookup system
                    (roles/roles-key token)
                    #(roles/admin (config/get-access-control-url system)
                            token
                            user-id))
    (catch Exception e
      (log/error e)
      {:errors (ex-data e)})))

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-permission
  [system token user-id concept-id]
  (try
    (caching/lookup system
                    (permissions/permissions-key token concept-id)
                    #(permissions/concept
                      (config/get-access-control-url system)
                      token
                      user-id
                      concept-id))
    (catch Exception e
      (log/error e)
      (ex-data e))))

A supporting function for check-roles-permissions that handles the roles side of things.

(defn check-roles
  [system handler request route-roles user-token user-id]
  (log/debug "Checking roles annotated in routes ...")
  (let [lookup (cached-admin-role system user-token user-id)
        errors (:errors lookup)]
    (if errors
      (response/not-allowed errors/no-permissions errors)
      (if (admin-role? route-roles lookup)
        (handler request)
        (response/not-allowed errors/no-permissions)))))

A supporting function for check-roles-permissions that handles the permissions side of things.

(defn check-permissions
  [system handler request route-permissions user-token user-id]
  (let [concept-id (permissions/route-concept-id request)
        lookup (cached-concept-permission
                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 (concept-permission? route-permissions
                               lookup
                               concept-id)
        (handler request)
        (response/not-allowed errors/no-permissions)))))

A supporting function for check-route-access that handles the actual checking.

(defn check-roles-permissions
  [system handler request route-roles route-permissions]
  (if-let [user-token (token/extract request)]
    (let [user-lookup (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)))))

Component Lifecycle Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defrecord Authz [])
(defn start
  [this]
  (log/info "Starting authorization component ...")
  (log/debug "Started authorization component.")
  this)
(defn stop
  [this]
  (log/info "Stopping authorization component ...")
  (log/debug "Stopped authorization component.")
  this)
(def lifecycle-behaviour
  {:start start
   :stop stop})
(extend Authz
  component/Lifecycle
  lifecycle-behaviour)

Component Constructor ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn create-component
  []
  (map->Authz {}))
 
(ns cmr.opendap.components.config
  (:require
   [cmr.authz.components.config :as authz-config]
   [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)))
(def cache-dumpfile #'authz-config/cache-dumpfile)
(def cache-init #'authz-config/cache-init)
(def cache-lru-threshold #'authz-config/cache-lru-threshold)
(def cache-ttl-ms #'authz-config/cache-ttl-ms)
(def cache-type #'authz-config/cache-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 #'authz-config/get-access-control-url)
(def get-echo-rest-url #'authz-config/get-echo-rest-url)
(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 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}
   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-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.http.kit.request :as request]
   [cmr.opendap.components.config :as config]
   [cmr.opendap.const :as const]
   [taoensso.timbre :as log])
  (:refer-clojure :exclude [get]))

Backwards-compatible Aliases ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def get-header request/get-header)
(def add-header request/add-header)
(def add-accept request/add-accept)
(def add-token-header request/add-token-header)
(def add-content-type request/add-content-type)
(def add-form-ct request/add-form-ct)
(def add-payload request/add-payload)
(def options request/options)

Header Support ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn add-user-agent
  ([]
    (add-user-agent {}))
  ([req]
    (request/add-header req "User-Agent" const/user-agent)))
(defn add-client-id
  ([]
    (add-client-id {}))
  ([req]
    (request/add-header req "Client-Id" const/client-id)))

HTTP Client Support ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def default-options
  (-> {:user-agent const/user-agent
       :insecure? true}
      (add-user-agent)
      (add-client-id)))
(defn request
  [method url req & [callback]]
  (request/request method url req default-options 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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(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 request/accept-pattern)
       (zipmap request/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.authz.errors :as authz-errors]
   [cmr.http.kit.response :as response]
   [cmr.opendap.errors :as errors]
   [ring.util.http-response :as ring-response]
   [taoensso.timbre :as log]
   [xml-in.core :as xml-in])
  (:refer-clojure :exclude [error-handler]))

Backwards-compatible Aliases ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def parse-json-body response/parse-json-body)
(def json-errors response/json-errors)
(def parse-xml-body response/parse-xml-body)
(def xml-errors response/xml-errors)
(def ok response/ok)
(def not-found response/not-found)
(def cors response/cors)
(def add-header response/add-header)
(def version-media-type response/version-media-type)
(def errors response/errors)
(def error response/error)
(def not-allowed response/not-allowed)

Custom Response Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn error-handler
  [status headers body]
  (response/error-handler status headers body (format errors/status-code status)))
(defn client-handler
  ([response]
    (client-handler response identity))
  ([response parse-fn]
    (response/client-handler response error-handler parse-fn)))
(def json-handler #(client-handler % response/parse-json-body))
(defn process-ok-results
  [data]
  {:headers {"CMR-Took" (:took data)
             "CMR-Hits" (:hits data)}
   :status 200})
(defn process-err-results
  [data]
  (cond (authz-errors/any-errors? data)
        {:status authz-errors/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))
      (ring-response/content-type "application/json")))
(defn text
  [_request data]
  (-> data
      process-results
      (assoc :body data)
      (ring-response/content-type "text/plain")))
(defn html
  [_request data]
  (-> data
      process-results
      (assoc :body data)
      (ring-response/content-type "text/html")))
 
(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 page namespace are only ever meant to be used in the cmr.search.site namespace, particularly in support of creating site routes for access in a browser.

Under no circumstances should cmr.search.site.data be accessed from outside this context; the data functions defined herein are specifically for use in page templates, structured explicitly for their needs.

(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 :cli variant uses a special constructed context (see static.StaticContext).

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.

Note: this function originally had more in it, but was moved into cmr.authz.

(defn service-keys
  [^Keyword service]
  [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)
              "/")))