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.esri.geometry/esri-geometry-api
2.2.0
com.stuartsierra/component
0.3.2
com.vividsolutions/jts
1.13
environ
1.1.0
gov.nasa.earthdata/cmr-authz
0.1.1-SNAPSHOT
gov.nasa.earthdata/cmr-http-kit
0.1.1-SNAPSHOT
gov.nasa.earthdata/cmr-mission-control
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.3
metosin/reitit-ring
0.1.3
metosin/ring-http-response
0.9.0
net.sf.geographiclib/GeographicLib-Java
1.49
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
org.geotools/gt-geometry
19.1
org.geotools/gt-referencing
19.1
ring/ring-core
1.6.3
ring/ring-codec
1.1.1
ring/ring-defaults
0.3.2
selmer
1.11.8
tolitius/xml-in
0.1.0



(this space intentionally left almost blank)
 

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]
   [cheshire.generate :as json-gen]
   [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])
  (:import
    (java.lang.ref SoftReference))
  (: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)

Utility functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Given a soft reference object and a Cheshire JSON generator, write the data stored in the soft reference to the generator as a JSON string.

Note, however, that sometimes the value is not a soft reference, but rather a raw value from the response. In that case, we need to skip the object conversion, and just do the realization.

(defn soft-reference->json!
  [obj json-generator]
  (let [data @(if (isa? obj SoftReference)
                (.get obj)
                obj)
        data-str (json/generate-string data)]
    (log/trace "Encoder got data: " data)
    (.writeString json-generator data-str)))

Global operations ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

This adds support for JSON-encoding the data cached in a SoftReference.

(json-gen/add-encoder SoftReference soft-reference->json!)

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]
  (log/trace "Got data for JSON:" 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.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 {}))
  ([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))))
 
(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))
(defn promise?
  [p]
  (isa? (class p) clojure.lang.IPending))
 
(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))
 

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

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 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))
 
(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.core
  (:require
   [clojure.set :as set]
   [clojure.string :as string]
   [cmr.opendap.components.concept :as concept]
   [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.util.geog :as geog]
   [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]
  (geog/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)]
    (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)
        ; grans-promise (concept/get :granules
        ;                component search-endpoint user-token params)
        coll-promise (concept/get :collection
                      component 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
  [component 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
  [component 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 grans-promise coll-promise s1-errs]
        (stage1 component
                search-endpoint
                user-token
                raw-params)
        ;; Stage 2
        [coll params data-files service-ids vars s2-errs]
        (stage2 component
                search-endpoint
                user-token
                params
                coll-promise
                grans-promise)
        ;; Stage 3
        [services bounding-info s3-errs]
        (stage3 component
                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 grans-promise coll-promise 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);

XXX The following set and function 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 ...")
  (log/trace "Collection data:" coll)
  (let [dataset-id (:dataset_id coll)
        reversed? (contains? lat-reversed-datasets dataset-id)]
    (log/debug "Data set id:" dataset-id)
    (if reversed?
      (log/debug "Identfied data set as having reversed latitude order ...")
      (log/debug "Identfied data set as having normal latitude order ..."))
    ;; 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
    reversed?))

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}
    bounding-box
    opts]
   (log/trace "Got dimensions:" dimensions)
   (when bounding-box
     (geog/bounding-box->lookup-record
      lon-max lat-max bounding-box (:reversed? opts)))))
(defn replace-defaults-lat-lon
  [bounding-info stride [k v]]
  (cond (= k :Longitude) (geog/format-opendap-dim-lon
                          (:opendap bounding-info) stride)
        (= k :Latitude) (geog/format-opendap-dim-lat
                         (:opendap bounding-info) stride)
        :else (geog/format-opendap-dim 0 stride (dec v))))
(defn format-opendap-dims
  ([bounding-info]
    (format-opendap-dims bounding-info geog/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-names
  [bounding-info]
  (log/debug "Original dimensions:" (:original-dimensions bounding-info))
  [(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")
   (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")])
(defn format-opendap-lat-lon
  ([bounding-info]
   (format-opendap-lat-lon bounding-info geog/default-lat-lon-stride))
  ([bounding-info stride]
   (geog/format-opendap-lat-lon (:opendap bounding-info)
                                (get-lat-lon-names bounding-info)
                                stride)))
(defn format-opendap-bounds
  ([bounding-info]
   (format-opendap-bounds bounding-info geog/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))
          ]
      (geog/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.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.util.geog
  (:require
   [cmr.opendap.const :as const]
   [taoensso.timbre :as log]))

Constants/Default Values ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def default-dim-stride 1)
(def default-lat-lon-stride 1)

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

Support/Utility Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(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)))))
(defn format-opendap-dim
  [min stride max]
  (if (or (nil? min) (nil? max))
    (format "[%s:%s:%s]" min stride max)))
(defn format-opendap-dim-lat
  ([lookup-record]
   (format-opendap-dim-lat lookup-record default-lat-lon-stride))
  ([lookup-record stride]
   (format-opendap-dim (get-in lookup-record [:low :lat])
                       stride
                       (get-in lookup-record [:high :lat]))))
(defn format-opendap-dim-lon
  ([lookup-record]
   (format-opendap-dim-lon lookup-record default-lat-lon-stride))
  ([lookup-record stride]
   (format-opendap-dim (get-in lookup-record [:low :lon])
                       stride
                       (get-in lookup-record [:high :lon]))))
(defn format-opendap-lat-lon
  ([lookup-record [lon-name lat-name] stride]
    (format "%s%s,%s%s"
            lat-name
            (format-opendap-dim-lat lookup-record stride)
            lon-name
            (format-opendap-dim-lon lookup-record stride))))

Core Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

This is the convenience constructor for the ArrayLookup record, taking latitude and longitude values and outputing a data structure that can be used for creating the lookup indices for OPeNDAP dimensional arrays. It has can create output for both normal and reversed latitudinal arrays:

  • Pass the reversed? parameter with a value of false 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). This is the default, when no value is passed for the reversed? parameter.

  • Pass the reversed? parameter with a value of true 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-array-lookup
  ([lon-lo lat-lo lon-hi lat-hi]
    (create-array-lookup lon-lo lat-lo lon-hi lat-hi false))
  ([lon-lo lat-lo lon-hi lat-hi reversed?]
    (let [lookup (map->ArrayLookup
                  {:low {:lon lon-lo
                         :lat lat-lo}
                   :high {:lon lon-hi
                          :lat lat-hi}})
          reversed-hi-lat (get-in lookup [:high :lat])
          reversed-lo-lat (get-in lookup [:low :lat])]
      (if reversed?
        (-> lookup
            (assoc-in [:low :lat] reversed-hi-lat)
            (assoc-in [:high :lat] reversed-lo-lat))
        lookup))))
(defn bounding-box->lookup-record
  ([bounding-box reversed?]
    (bounding-box->lookup-record const/default-lon-abs-hi
                                 const/default-lat-abs-hi
                                 bounding-box
                                 reversed?))
  ([lon-max lat-max
   [lon-lo lat-lo lon-hi lat-hi :as bounding-box]
   reversed?]
   (let [lon-lo (lon-lo-phase-shift lon-max lon-lo)
         lon-hi (lon-hi-phase-shift lon-max lon-hi)]
     (if reversed?
       (let [lat-lo (lat-lo-phase-shift-reversed lat-max lat-lo)
             lat-hi (lat-hi-phase-shift-reversed lat-max lat-hi)]
         (log/debug "Variable latitudinal values are reversed ...")
         (create-array-lookup lon-lo lat-lo lon-hi lat-hi reversed?))
       (let [lat-lo (lat-lo-phase-shift lat-max lat-lo)
             lat-hi (lat-hi-phase-shift lat-max lat-hi)]
         (create-array-lookup lon-lo lat-lo lon-hi lat-hi))))))
(defn bounding-box->lookup-indices
  ([bounding-box]
    (bounding-box->lookup-indices bounding-box ["Longitude" "Latitude"]))
  ([bounding-box index-names]
    (bounding-box->lookup-indices bounding-box false index-names))
  ([bounding-box reversed? index-names]
    (bounding-box->lookup-indices bounding-box
                                  reversed?
                                  index-names
                                  default-lat-lon-stride))
  ([bounding-box reversed? index-names stride]
    (bounding-box->lookup-indices const/default-lon-abs-hi
                                  const/default-lat-abs-hi
                                  bounding-box
                                  reversed?
                                  index-names
                                  stride))
  ([lon-max lat-max bounding-box reversed? index-names stride]
    (format-opendap-lat-lon
      (bounding-box->lookup-record bounding-box reversed?)
      index-names
      stride)))
 
(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
  [concept-id]
  (str "concept_id=" concept-id))

Given a data structure with :collection-id, get the metadata for the associated collection.

(defn async-get-metadata
  [search-endpoint user-token params]
  (let [concept-id (:collection-id params)
        url (str search-endpoint
                 "/collections?"
                 (build-query concept-id))]
    (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.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])
 
(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)))))
 

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.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)
         ((fn [x] (log/debug "Client request options:" x) x)))
     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))]})))
 

Equations taken from the following: * http://earth-info.nga.mil/GandG/publications/tr8350.2/wgs84fin.pdf

(ns cmr.opendap.geom.util)

Constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def earth-semi-major-axis 6378137.0)
(def earth-semi-minor-axis 6356752.3142)
(def earth-semi-major-axis**2 (Math/pow earth-semi-major-axis 2))
(def earth-semi-minor-axis**2 (Math/pow earth-semi-minor-axis 2))
(def earth-radius earth-semi-major-axis)
(def earth-radius**2 earth-semi-major-axis**2)
(def earth-area (* 4 Math/PI earth-radius**2))
(def earth-eccentricity 0.081819190842622)
(def earth-eccentricity**2 (Math/pow earth-eccentricity 2))
(def earth-linear-eccentricity 521854.00842339)

Supporting Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn prime-vertical-curvature-radius
  [lat-radians]
  (/ earth-semi-major-axis
     (Math/sqrt (- 1
                   (* earth-eccentricity**2
                      (Math/pow (Math/sin lat-radians) 2))))))

Conversion Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Taken from: * https://www.mathworks.com/matlabcentral/fileexchange/7942-covert-lat--lon--alt-to-ecef-cartesian

Inputs are in degrees and meters; outputs are in meters.

(defn lla->ecef
  [lat lon alt]
  (let [lat-radians (Math/toRadians lat)
        lon-radians (Math/toRadians lon)
        n (prime-vertical-curvature-radius lat-radians)
        n-alt (+ n alt)
        x (* n-alt
            (Math/cos lat-radians)
            (Math/cos lon-radians))
        y (* n-alt
             (Math/cos lat-radians)
             (Math/sin lon-radians))
        z (* (+ (* n
                   (- 1 earth-eccentricity**2))
                alt)
             (Math/sin lat-radians))]
    [x y z]))

Taken from: * https://www.mathworks.com/matlabcentral/fileexchange/7941-convert-cartesian--ecef--coordinates-to-lat--lon--alt

Currently no correction for instability in altitude near exact poles.

Inputs are in meters; outputs are degrees and meters.

(defn ecef->lla
  [x y z]
  (let [ep (Math/sqrt (/ (- earth-semi-major-axis**2
                            earth-semi-minor-axis**2)
                         earth-semi-minor-axis**2))
        p (Math/sqrt (+ (Math/pow x 2) (Math/pow y 2)))
        theta (Math/atan2 (* earth-semi-major-axis z)
                       (* earth-semi-minor-axis p))
        lon (Math/atan2 y x)
        lat (Math/atan2 (+ z (* (Math/pow ep 2)
                                earth-semi-minor-axis
                                (Math/pow
                                 (Math/sin theta) 3)))
                        (- p (* earth-eccentricity**2
                                earth-semi-major-axis
                                (Math/pow
                                 (Math/cos theta) 3))))
        n (prime-vertical-curvature-radius lat)
        alt (- (/ p (Math/cos lat)) n)]
    [(Math/toDegrees lat)
     (Math/toDegrees (mod lon (* 2 Math/PI)))
     alt]))

Inputs are in degrees; outputs are in meters.

(defn ll->cartesian
  [lat lon]
  [(/ (* Math/PI earth-radius lon) 180)
   (* earth-radius (Math/sin (Math/toRadians lat)))])

Assumes CCW ordering of points.

(defn bbox->polypoints
  [bbox])
 

See the following links: * http://esri.github.io/geometry-api-java/javadoc/ * https://github.com/Esri/geometry-api-java/wiki

(ns cmr.opendap.geom.impl.esri
  (:require
   [cmr.opendap.geom.util :as util])
  (:import
   (com.esri.core.geometry Polygon))
  (:refer-clojure :exclude [intersection]))

Utility Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- -add-points!
  [self points]
  (->> points
       (partition 2)
       (mapv (fn [[lat lon]]
               (let [[x y] (util/ll->cartesian lat lon)]
                 (.lineTo self x y))))))

API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Returns area in m^2 units.

(defn area
  [this]
  (.calculateArea2D this))
(defn intersection
  [this other])
(def behaviour {:area area
                :intersection intersection})

Polygon points are provided in counter-clockwise order. The last point should match the first point to close the polygon. The values are listed comma separated in longitude latitude order, i.e.:

[lon1 lat1 lon2 lat2 lon3 lat3 ...]

Returns area in m^2 units.

(defn create
  [[first-lat first-lon & points]]
  (let [polygon (new Polygon)
        [start-x start-y] (util/ll->cartesian first-lat first-lon)]
    (.startPath polygon start-x start-y)
    (-add-points! polygon points)
    polygon))
 

See the following: * http://www.tsusiatsoftware.net/jts/javadoc/com/vividsolutions/jts/geom/Geometry.html

(ns cmr.opendap.geom.impl.jts
  (:require
   [cmr.opendap.geom.util :as util])
  (:import
   (org.geotools.geometry.jts JTS)
   (org.geotools.referencing CRS)
   (org.geotools.referencing.crs DefaultGeocentricCRS DefaultGeographicCRS)
   (com.vividsolutions.jts.geom Coordinate GeometryFactory PrecisionModel))
  (:refer-clojure :exclude [empty? intersection reverse]))

Constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def cartesian-srid 0)
(def wgs84-srid 1)
(def srids {cartesian-srid :cartesian
            wgs84-srid :wgs84})

Utility Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- -create-coords
  [points]
  (->> points
       (partition 2)
       (mapv (fn [[lat lon]]
         (let [[x y] (util/ll->cartesian lat lon)]
          (new Coordinate x y 0))))
       (into-array)))
(def wgs84->cartesian-xform (CRS/findMathTransform
                             DefaultGeographicCRS/WGS84_3D
                             DefaultGeocentricCRS/CARTESIAN
                             true))
(def cartesian->wgs84-xform (CRS/findMathTransform
                             DefaultGeocentricCRS/CARTESIAN
                             DefaultGeographicCRS/WGS84_3D
                             true))

API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Returns area in m^2 units.

(defn area
  [this]
  (.getArea this))
(defn bounding-box
  [this]
  (.getEnvelope this))
(defn empty?
  [this]
  (.isEmpty this))
(defn intersection
  [this other]
  (.intersection this other))
(defn intersects?
  [this other]
  (.intersects this other))
(defn points
  [this]
  (mapv #(vector (.-x %) (.-y %) (.-z %)) (.getCoordinates this)))
(defn point-count
  [this]
  (.getNumPoints this))
(defn reverse
  [this]
  (.reverse this))
(defn valid?
  [this]
  (.isValid this))
(defn cartesian->wgs84
  [this]
  (JTS/transform this cartesian->wgs84-xform))
(defn wgs84->cartesian
  [this]
  (JTS/transform this wgs84->cartesian-xform))
(def behaviour {:area area
                :bounding-box bounding-box
                :empty? empty?
                :intersection intersection
                :intersects? intersects?
                :points points
                :point-count point-count
                :reverse reverse
                :valid? valid?
                ;; Experimental
                :cartesian->wgs84 cartesian->wgs84
                :wgs84->cartesian wgs84->cartesian})

Polygon points are provided in counter-clockwise order. The last point should match the first point to close the polygon. The values are listed comma separated in longitude latitude order, i.e.:

[lon1 lat1 lon2 lat2 lon3 lat3 ...]
(defn create
  [points]
  (let [factory (new GeometryFactory (new PrecisionModel PrecisionModel/FLOATING)
                                     wgs84-srid)]
    (.createPolygon factory (-create-coords points))))
 

See the following: * https://geographiclib.sourceforge.io/html/java/ * https://sourceforge.net/p/geographiclib/code/ci/release/tree/java/planimeter/src/main/java/Planimeter.java#l6

(ns cmr.opendap.geom.impl.geographiclib
  (:import
   (net.sf.geographiclib Geodesic PolygonArea))
  (:refer-clojure :exclude [intersection]))

Constants ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def default-geodesic Geodesic/WGS84)
(def default-polyline? false) ; if a closed polygon, polyline needs to be false

Utility Functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- -add-points!
  [self points]
  (mapv (fn [[lat lon]] (.AddPoint self lat lon)) (partition 2 points)))

API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defrecord GeographiclibPolygon [native])

Returns area in m^2 units.

(defn area
  [this]
  (-> this
      :native
      (.Compute)
      (.area)))
(defn intersection
  [this other])
(def behaviour {:area area
                :intersection intersection})

Polygon points are provided in counter-clockwise order. The last point should match the first point to close the polygon. The values are listed comma separated in longitude latitude order, i.e.:

[lon1 lat1 lon2 lat2 lon3 lat3 ...]

Returns area in m^2 units.

(defn create
  ([points]
   (create points default-geodesic))
  ([points geodesic]
   (create points geodesic default-polyline?))
  ([points geodesic polyline?]
   (let [polygon (new PolygonArea geodesic polyline?)]
     (-add-points! polygon points)
     (map->GeographiclibPolygon {:native polygon}))))
 
(ns cmr.opendap.geom.core
  (:require
    [cmr.opendap.geom.impl.esri :as esri]
    [cmr.opendap.geom.impl.geographiclib :as geographiclib]
    [cmr.opendap.geom.impl.jts :as jts])
  (:import
    (com.esri.core.geometry.Polygon)
    (com.vividsolutions.jts.geom.Polygon)
    (cmr.opendap.geom.impl.geographiclib GeographiclibPolygon))
  (:refer-clojure :exclude [empty? intersection reverse]))
(defprotocol PolygonAPI
  (area [this])
  (bounding-box [this])
  (empty? [this])
  (intersection [this other])
  (intersects? [this other])
  (points [this])
  (point-count [this])
  (reverse [this])
  (valid? [this])
  ;; Experimental
  (cartesian->wgs84 [this])
  (wgs84->cartesian [this]))
(extend com.vividsolutions.jts.geom.Polygon
        PolygonAPI
        jts/behaviour)
(extend com.esri.core.geometry.Polygon
        PolygonAPI
        esri/behaviour)
(extend GeographiclibPolygon
        PolygonAPI
        geographiclib/behaviour)
(defn create-polygon
  [type points]
  (case type
    :jts (jts/create points)
    :esri (esri/create points)
    :geographiclib (geographiclib/create points)))
 
(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.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)
              "/")))
 
(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.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))))
 

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

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 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.concept-cache :as concept-cache-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
    [;; Authz cache
     ["/opendap/cache/auth" {
      :get {:handler (auth-cache-handler/lookup-all httpd-component)
            :roles #{:admin}}
      :delete {:handler (auth-cache-handler/evict-all httpd-component)
               :roles #{:admin}}}]
     ["/opendap/cache/auth/:item-key" {
      :get {:handler (auth-cache-handler/lookup httpd-component)
            :roles #{:admin}}
      :delete {:handler (auth-cache-handler/evict httpd-component)
               :roles #{:admin}}}]
     ;; Concept cache
     ["/opendap/cache/concept" {
      :get {:handler (concept-cache-handler/lookup-all httpd-component)
            :roles #{:admin}}
      :delete {:handler (concept-cache-handler/evict-all httpd-component)
               :roles #{:admin}}}]
     ["/opendap/cache/concept/:item-key" {
      :get {:handler (concept-cache-handler/lookup httpd-component)
            :roles #{:admin}}
      :delete {:handler (concept-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))
 

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))
 
(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.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 handlers for the cache REST API resources.

(ns cmr.opendap.app.handler.concept-cache
  (:require
   [clojure.java.io :as io]
   [clojusc.twig :as twig]
   [cmr.opendap.components.caching :as caching]
   [cmr.opendap.http.response :as response]
   [taoensso.timbre :as log]))
(defn lookup-all
  [component]
  (fn [request]
    (->> component
         caching/lookup-all
         (response/json request))))
(defn evict-all
  [component]
  (fn [request]
    (log/debug "Evicting all cached items ...")
    (->> component
         caching/evict-all
         (response/json request))))
(defn lookup
  [component]
  (fn [request]
    (let [item-key (get-in request [:path-params :item-key])]
      (response/json
       request
       (caching/lookup component item-key)))))
(defn evict
  [component]
  (fn [request]
    (let [item-key (get-in request [:path-params :item-key])]
      (log/debugf "Evicting value cached at key %s ..." item-key)
      (caching/evict component item-key)
      (response/json
       request
       (caching/lookup component item-key)))))
 

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

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)))))))
 
(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 exception-data
  [exception]
  [(or (.getMessage exception)
       (ex-data exception))])
(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.data.size.util)
(def kb (Math/pow 2 10))
(def mb (Math/pow 2 20))
(def gb (Math/pow 2 30))
(defn kb->bytes
  [value]
  (* kb value))
(defn mb->bytes
  [value]
  (* mb value))
(defn bytes->mb
  [value]
  (/ value mb))
(defn bytes->gb
  [value]
  (/ value gb))
 
(ns cmr.opendap.data.size.granule
  (:require
   [clojure.data.xml :as xml]
   [cmr.opendap.data.size.util :as util]
   [xml-in.core :as xml-in]))
(defn extract-size-data
  [parsed-xml]
  (-> parsed-xml
      (xml-in/find-first [:Granule :DataGranule :SizeMBDataGranule])
      first
      read-string
      util/mb->bytes))

Returns granule file size in bytes.

(defn file-size
  [xml-metadata]
  (-> xml-metadata
      xml/parse-str
      extract-size-data))
 

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]
   [cmr.opendap.errors :as cmro-errors]
   [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)
      {:errors (cmro-errors/exception-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 (cmro-errors/exception-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)
      {:errors (cmro-errors/exception-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
      (do
        (log/error errors/no-permissions)
        (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
      (do
        (log/error errors/no-permissions)
        (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
        (do
          (log/error errors/token-required)
          (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 {}))
 

A component system setup namespace for use in testing.

(ns cmr.opendap.components.testing.system
  (:require
    [cmr.opendap.components.core :as core]))

Component Initialization ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn init
  ([]
    (init :testing))
  ([mode]
    ((mode core/init-lookup))))
 

A component system setup namespace for use in testing.

(ns cmr.opendap.components.testing.config
  (:require
    [cmr.opendap.components.core :as core]))

Component Initialization ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn init
  ([]
    (init :testing-config-only))
  ([mode]
    ((mode core/init-lookup))))
 
(ns cmr.opendap.components.config
  (:require
   [cmr.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 authz-cache-dumpfile #'authz-config/cache-dumpfile)
(def authz-cache-init #'authz-config/cache-init)
(def authz-cache-lru-threshold #'authz-config/cache-lru-threshold)
(def authz-cache-ttl-ms #'authz-config/cache-ttl-ms)
(def authz-cache-type #'authz-config/cache-type)
(defn concept-cache-dumpfile
  [system]
  (get-in (get-cfg system) [:concept-caching :dumpfile]))
(defn concept-cache-init
  [system]
  (get-in (get-cfg system) [:concept-caching :init]))
(defn concept-cache-ttl-ms
  [system]
  (* (get-in (get-cfg system) [:concept-caching :ttl :hours])
     60 ; minutes
     60 ; seconds
     1000 ; milliseconds))
(defn cache-type
  [system]
  (get-in (get-cfg system) [:auth-caching :type]))
(defn cmr-max-pagesize
  [system]
  (get-in (get-cfg system) [:cmr :max-pagesize]))
(defn get-service
  [system service]
  (let [svc-cfg (get-in (get-cfg system)
                        (concat [:cmr] (config/service-keys service)))]
    svc-cfg))
(defn cmr-base-url
  [system]
  (config/service->base-url (get-service system :search)))

This function returns the cmr-opendap URL with a trailing slash, but without the 'opendap' appended.

(defn opendap-base-url
  [system]
  (str (config/service->base-public-url (get-service system :opendap)) "/"))

This function returns the cmr-opendap URL with a trailing slash.

(defn opendap-url
  [system]
  (str (config/service->public-url (get-service system :opendap)) "/"))
(defn get-service-url
  [system service]
  (config/service->url (get-service system service)))

The URLs returned by these functions have no trailing slash:

(def get-access-control-url #'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/trace "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.caching
  (:require
   [clojure.core.cache :as cache]
   [clojure.java.io :as io]
   [cmr.opendap.components.config :as config]
   [com.stuartsierra.component :as component]
   [taoensso.timbre :as log]))

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

(defn load-cache
  [system]
  (if-let [sys system]
    (if-let [filename (config/concept-cache-dumpfile system)]
      (try
        (read-string
          (slurp filename))
        (catch Exception _ nil)))))
(defn dump-cache
  [system cache-data]
  (let [dumpfile (config/concept-cache-dumpfile system)]
    (io/make-parents dumpfile)
    (spit
      dumpfile
      (prn-str cache-data))))
(defn item-has-value?
  [item]
  (cond (nil? item) false
        (and (seq? item) (empty? item)) false
        :else true))

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

(defn create-cache
  ([system]
   (create-cache system
                 (merge (config/concept-cache-init system)
                        (load-cache system))))
  ([system init-items]
   (let [cache (-> init-items
                   cache/soft-cache-factory ; will GC with memory pressyre
                   cache/lru-cache-factory)]
     (log/debug "Creating memory-sensitive cache composed with LRU cache ...")
     (log/trace "Starting value:" init-items)
     cache)))
(defn get-cache
  [system]
  (get-in system [:concept-caching :cache]))
(defn evict
  [system item-key]
  (swap! (get-cache system) cache/evict item-key))
(defn evict-all
  [system]
  (reset! (get-cache system)
          (create-cache system (config/concept-cache-init system))))
(defn has?
  [system item-key]
  (cache/has? @(get-cache system) item-key))
(defn- -has-all?
  [ch item-keys]
  (every? #(cache/has? ch %) item-keys))
(defn has-all?
  [system item-keys]
  (let [ch @(get-cache system)]
    (-has-all? ch item-keys)))
(defn lookup
  ([system item-key]
    (cache/lookup @(get-cache system) item-key))
  ([system item-key value-fn]
    (let [ch @(get-cache system)]
      (if (cache/has? ch item-key)
        (do
          (log/debug "Concept cache has key; skipping value function ...")
          (log/trace "Key:" item-key)
          (cache/hit ch item-key))
        (when-let [value (value-fn)]
          (log/debug "Concept cache miss; calling value function ...")
          (when (item-has-value? value)
            (swap! (get-cache system) #(cache/miss % item-key value))))))
    (lookup system item-key)))
(defn lookup-many
  ([system item-keys]
    (let [ch @(get-cache system)]
      (map #(cache/lookup ch %) item-keys)))
  ([system item-keys value-fn]
    (let [ch @(get-cache system)]
      (if (-has-all? ch item-keys)
        (do
          (log/debug "Concept cache has all keys; skipping value function ...")
          (log/trace "Keys:" item-keys)
          (mapv #(cache/hit ch %) item-keys))
        (when-let [key-values-map (value-fn)]
          (log/debug (str "Concept cache miss for at least one key; "
                          "calling value function ..."))
          (dorun
            (for [[k v] key-values-map]
              (when (item-has-value? v)
                (swap! (get-cache system) #(cache/miss % k v))))))))
    (lookup-many system item-keys)))
(defn lookup-all
  [system]
  @(get-cache system))

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

(defrecord ConceptCaching [cache])
(defn start
  [this]
  (log/info "Starting concept caching component ...")
  (let [cache (atom (create-cache this))]
    (log/debug "Started concept caching component.")
    (assoc this :cache cache)))
(defn stop
  [this]
  (log/info "Stopping concept caching component ...")
  (if-let [cache-ref (:cache this)]
    (if-let [cache @cache-ref]
      (dump-cache this cache)))
  (log/debug "Stopped concept caching component.")
  (assoc this :cache nil))
(def lifecycle-behaviour
  {:start start
   :stop stop})
(extend ConceptCaching
  component/Lifecycle
  lifecycle-behaviour)

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

(defn create-component
  []
  (map->ConceptCaching {}))
 
(ns cmr.opendap.components.core
  (:require
    [cmr.authz.components.caching :as auth-caching]
    [cmr.mission-control.components.pubsub :as pubsub]
    [cmr.opendap.components.auth :as auth]
    [cmr.opendap.components.caching :as concept-caching]
    [cmr.opendap.components.concept :as concept]
    [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 pubsub
  {:pubsub (component/using
            (pubsub/create-component)
            [:config :logging])})
(def auth-cache
  {:auth-caching (component/using
                  (auth-caching/create-component)
                  [:config :logging])})
(def authz
  {:auth (component/using
          (auth/create-component)
          [:auth-caching :pubsub])})
(def concept-cache
  {:concept-caching (component/using
                     (concept-caching/create-component)
                     [:config :logging])})
(def concepts
  {:concepts (component/using
              (concept/create-component)
              [:concept-caching :pubsub])})
(def httpd
  {:httpd (component/using
           (httpd/create-component)
           [:config :logging :pubsub
            :auth-caching :auth
            :concept-caching :concepts])})

Additional components for systems that want to supress logging (e.g., systems created for testing).

(def pubsub-without-logging
  {:pubsub (component/using
            (pubsub/create-component)
            [:config])})
(def auth-cache-without-logging
  {:auth-caching (component/using
                  (auth-caching/create-component)
                  [:config])})
(def concept-cache-without-logging
  {:concept-caching (component/using
                     (concept-caching/create-component)
                     [:config])})
(def httpd-without-logging
  {:httpd (component/using
           (httpd/create-component)
           [:config :pubsub :auth-caching :auth :concept-caching :concepts])})

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

This namespace represents the 'concept query' API for CMR OPeNDAP. This is where the rest of the application goes when it needs to perform a query to CMR to get concept data. This is done in order to cache concepts and use these instead of making repeated queries to the CMR.

(ns cmr.opendap.components.concept
  (:require
   [clojure.string :as string]
   [cmr.opendap.components.caching :as caching]
   [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.util :as util]
   [com.stuartsierra.component :as component]
   [taoensso.timbre :as log])
  (:refer-clojure :exclude [get]))

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

(defn concept-key
  [id]
  (str "concept:" id))
(defn- -get-single-cached
  [system cache-key lookup-fn lookup-args]
  (try
    (caching/lookup
     system
     cache-key
     #(apply lookup-fn lookup-args))
    (catch Exception e
      (log/error e)
      {:errors (errors/exception-data e)})))
(defn- -get-multiple-cached
  [system cache-keys lookup-fn lookup-args]
  (try
    (caching/lookup-many
      system
      cache-keys
      #(apply lookup-fn lookup-args))
    (catch Exception e
      (log/error e)
      {:errors (errors/exception-data e)})))

This does the actual work for the cache lookup and fallback function call.

(defn- -get-cached
  ([system cache-key lookup-fn lookup-args]
   (-get-cached system cache-key lookup-fn lookup-args {}))
  ([system cache-key lookup-fn lookup-args opts]
   (let [multi-key? (:multi-key? opts)]
     (log/trace "lookup-fn:" lookup-fn)
     (log/trace "lookup-args:" lookup-args)
     (log/trace "Cache key(s):" cache-key)
     (if multi-key?
       (-get-multiple-cached system cache-key lookup-fn lookup-args)
       (-get-single-cached system cache-key lookup-fn lookup-args)))))

Look up the concept for a concept-id in the cache; if there is a miss, make the actual call for the lookup.

Due to the fact that the results may or may not be a promise, this function will check to see if the value needs to be wrapped in a promise and will do so if need be.

(defn get-cached
  ([system cache-key lookup-fn lookup-args]
   (get-cached system cache-key lookup-fn lookup-args {}))
  ([system cache-key lookup-fn lookup-args opts]
   (let [maybe-promise (-get-cached system
                        cache-key lookup-fn lookup-args opts)]
     (if (util/promise? maybe-promise)
       (do
         (log/trace "Result identifed as promise ...")
         maybe-promise)
       (let [wrapped-data (promise)]
         (log/trace "Result is not a promise ...")
         (deliver wrapped-data maybe-promise)
         wrapped-data)))))

Concept Component API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defmulti get (fn [concept-type & _]
  (log/trace "Dispatching on concept type:" concept-type)
  concept-type))
(defmethod get :collection
  [_type system search-endpoint user-token params]
  (let [cache-key (concept-key (:collection-id params))]
    (get-cached system
                cache-key
                collection/async-get-metadata
                [search-endpoint user-token params])))
(defmethod get :granules
  [_type system search-endpoint user-token params]
  (let [collection (:collection-id params)
        granules (:granules params)
        expllicit-cache-keys (map #(concept-key (str collection ":" %))
                                  granules)
        implicit-cache-keys [(concept-key (str collection ":granules"))]]
    (get-cached system
                (if (seq granules) expllicit-cache-keys implicit-cache-keys)
                granule/async-get-metadata
                [search-endpoint user-token params]
                {:multi-key? true})))

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

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

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

(defn create-component
  []
  (map->Concept {}))
 
(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 {}))
 
(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.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)