Clojure Protocol Buffer Library

0.1.0-SNAPSHOT


A service and API for querying CMR metadata relationships

dependencies

cheshire
5.8.0
clojurewerkz/elastisch
3.0.0
clojurewerkz/neocons
3.2.0
clojusc/trifl
0.2.0
clojusc/twig
0.3.2
com.stuartsierra/component
0.3.2
digest
1.4.6
http-kit
2.2.0
metosin/reitit-core
0.1.1-SNAPSHOT
metosin/reitit-ring
0.1.1-SNAPSHOT
metosin/ring-http-response
0.9.0
org.clojure/clojure
1.9.0
org.clojure/data.csv
0.1.4
ring/ring-core
1.6.3
ring/ring-codec
1.1.0
ring/ring-defaults
0.3.1



(this space intentionally left almost blank)
 
(ns cmr.graph.demo.movie
  (:require
   [clojure.string :as string]
   [clojurewerkz.neocons.rest :as nr]
   [clojurewerkz.neocons.rest.cypher :as cy]
   [cmr.graph.queries.neo4j.demo.movie :as query]
   [taoensso.timbre :as log]))
(defn get-graph
  ([conn]
    (get-graph conn 100))
  ([conn limit]
    (when (string? limit)
      (get-graph conn (Integer/parseInt limit)))
    (let   [result (cy/tquery conn query/graph {:limit limit})
            nodes (map (fn [{:strs [cast movie]}]
                         (concat [{:title movie
                                   :label :movie}]
                                 (map (fn [x] {:title x
                                               :label :actor})
                                      cast)))
                       result)
            nodes (distinct (apply concat nodes))
            nodes-index (into {} (map-indexed #(vector %2 %1) nodes))
            links (map (fn [{:strs [cast movie]}]
                         (let [target   (nodes-index {:title movie :label :movie})]
                           (map (fn [x]
                                  {:target target
                                   :source (nodes-index {:title x :label :actor})})
                                       cast)))
                       result)]
    {:nodes nodes :links (flatten links)})))
(defn search
  [conn q]
  (if (string/blank? q)
    []
    (let  [result (cy/tquery conn query/search {:title (str "(?i).*" q ".*")})]
      (map (fn [x] {:movie (:data (x "movie"))}) result))))
(defn get-movie
  [conn title]
  (log/trace "Got connection:" conn)
  (log/trace "Got title:" title)
  (let [[result] (cy/tquery conn query/title {:title title})]
    result))
 
(ns cmr.graph.queries.neo4j.demo.movie)
(def graph "MATCH (m:Movie)<-[:ACTED_IN]-(a:Person)
            RETURN m.title as movie, collect(a.name) as cast
            LIMIT {limit};")
(def search "MATCH (movie:Movie) WHERE movie.title =~ {title} RETURN movie;")
(def title "MATCH (movie:Movie {title:{title}})
            OPTIONAL MATCH (movie)<-[r]-(person:Person)
            RETURN movie.title as title,
                   collect({name:person.name,
                            job:head(split(lower(type(r)),'_')),
                            role:r.roles}) as cast LIMIT 1;")
 
(ns cmr.graph.queries.neo4j.collections
  (:require
   [clojure.string :as string]))
(def reset "MATCH (n) DETACH DELETE n;")
(def get-all "MATCH (collection:Collection) RETURN collection;")
(def delete-all "MATCH (collection:Collection) DELETE collection;")
(def delete-all-cascade "MATCH (collection:Collection) DETACH DELETE collection;")
(defn get-urls-by-concept-id
  [concept-id]
  (format "match (c:Collection)-[:LINKS_TO]->(u:Url) where c.conceptId='%s' return u.name;"
          concept-id))
(defn get-concept-ids-by-urls
  [urls]
  (format "match (c:Collection)-[:LINKS_TO]->(u:Url) where u.name in [%s] return c.conceptId;"
          (string/join "," (map #(format "'%s'" %) urls))))
 
(ns cmr.graph.core
  (:require
   [clojusc.twig :as logger]
   [cmr.graph.components.core :as componemts]
   [com.stuartsierra.component :as component]
   [taoensso.timbre :as log]
   [trifl.java :as java])
  (:gen-class))
(def startup-message
  (str "Component startup complete."
       \newline \newline
       "The CMR Graph system is now ready to use. "
       "To get started,"
       \newline
       "you can visit the following:"
       \newline
       "* Neo4j: http://localhost:7474/browser/"
       \newline
       "* Kibana: http://localhost:5601/"
       \newline \newline
       "Additionally, the CMR Graph REST API is available at"
       \newline
       "http://localhost:3012. To try it out, call the following:"
       \newline
       "* curl --silent \"http://localhost:3012/ping\
       \newline
       "* curl --silent \"http://localhost:3012/health\
       \newline))
(defn shutdown
  [system]
  (component/stop system))
(defn -main
  [& args]
  (logger/set-level! ['cmr.graph] :info)
  (log/info "Starting the CMR Graph components ...")
  (let [system (componemts/init)]
    (component/start system)
    (java/add-shutdown-handler #(shutdown system)))
  (log/info startup-message))
 

CMR Graph system management.

(ns cmr.graph.system.impl.state
  (:require
    [com.stuartsierra.component :as component]
    [taoensso.timbre :as log]))

State Atom ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def ^:dynamic *state*
  (atom {:status :stopped
         :system nil
         :ns }))

System State Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defrecord StateTracker [])
(defn get-state
  [_this]
  @*state*)
(defn set-state
  [_this new-state]
  (reset! *state* new-state))
(defn get-status
  [this]
  (:status (get-state this)))
(defn set-status
  [this value]
  (set-state this (assoc (get-state this) :status value)))
(defn get-system
  [this]
  (:system (get-state this)))
(defn set-system
  [this value]
  (set-state this (assoc (get-state this) :system value)))
(defn get-system-ns
  [this]
  (:ns (get-state this)))
(defn set-system-ns
  [this an-ns]
  (set-state this (assoc (get-state this) :ns an-ns)))
(def behaviour
  {:get-state get-state
   :set-state set-state
   :get-status get-status
   :set-status set-status
   :get-system get-system
   :set-system set-system
   :get-system-ns get-system-ns
   :set-system-ns set-system-ns})
(defn create-state-tracker
  []
  (->StateTracker))
 

CMR Graph system management.

(ns cmr.graph.system.impl.management
  (:require
    [cmr.graph.system.impl.state :as state]
    [com.stuartsierra.component :as component]
    [taoensso.timbre :as log]))

Transition Vars ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def valid-stop-transitions #{:started :running})
(def invalid-init-transitions #{:initialized :started :running})
(def invalid-deinit-transitions #{:started :running})
(def invalid-start-transitions #{:started :running})
(def invalid-stop-transitions #{:stopped})
(def invalid-startup-transitions #{:running})
(def invalid-shutdown-transitions #{:uninitialized :shutdown})

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

(defn- resolve-by-name
  [an-ns a-fun]
  (resolve (symbol (str an-ns "/" a-fun))))
(defn- call-by-name
  [an-ns a-fun & args]
  (apply (resolve-by-name an-ns a-fun) args))

State Management Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defrecord StateManager [state])
(defn init
  ([this]
    (init this :default))
  ([this mode]
    (if (contains? invalid-init-transitions (state/get-status (:state this)))
      (log/warn "System has aready been initialized.")
      (do
        (state/set-system (:state this)
                          (call-by-name (state/get-system-ns (:state this))
                                        "init"))
        (state/set-status (:state this) :initialized)))
    (state/get-status (:state this))))
(defn deinit
  [this]
  (if (contains? invalid-deinit-transitions (state/get-status (:state this)))
    (log/error "System is not stopped; please stop before deinitializing.")
    (do
      (state/set-system (:state this) nil)
      (state/set-status (:state this) :uninitialized)))
  (state/get-status (:state this)))
(defn start
  ([this]
    (start this :default))
  ([this mode]
    (when (nil? (state/get-status (:state this)))
      (init mode))
    (if (contains? invalid-start-transitions (state/get-status (:state this)))
      (log/warn "System has already been started.")
      (do
        (state/set-system (:state this)
                          (component/start (state/get-system (:state this))))
        (state/set-status (:state this) :started)))
    (state/get-status (:state this))))
(defn stop
  [this]
  (if (contains? invalid-stop-transitions (state/get-status (:state this)))
    (log/warn "System already stopped.")
    (do
      (state/set-system (:state this)
                        (component/stop (state/get-system (:state this))))
      (state/set-status (:state this) :stopped)))
  (state/get-status (:state this)))
(defn restart
  ([this]
    (restart this :default))
  ([this mode]
    (stop this)
    (start this mode)))

Initialize a system and start all of its components.

This is essentially a convenience wrapper for init + start.

(defn startup
  ([this]
    (startup this :default))
  ([this mode]
    (if (contains? invalid-startup-transitions (state/get-status (:state this)))
      (log/warn "System is already running.")
      (do
        (when-not (contains? invalid-init-transitions
                             (state/get-status (:state this)))
          (init this mode))
        (when-not (contains? invalid-start-transitions
                            (state/get-status (:state this)))
          (start this mode))
        (state/set-status (:state this) :running)
        (state/get-status (:state this))))))
(defn shutdown
  [this]
  "Stop a running system and de-initialize it.
  This is essentially a convenience wrapper for `stop` + `deinit`."
  (if (contains? invalid-shutdown-transitions (state/get-status (:state this)))
    (log/warn "System is already shutdown.")
    (do
      (when-not (contains? invalid-stop-transitions
                           (state/get-status (:state this)))
        (stop this))
      (when-not (contains? invalid-deinit-transitions
                           (state/get-status (:state this)))
        (deinit this))
      (state/set-status (:state this) :shutdown)
      (state/get-status (:state this)))))
(def behaviour
  {:init init
   :deinit deinit
   :start start
   :stop stop
   :restart restart
   :startup startup
   :shutdown shutdown})
(defn create-state-manager
  []
  (->StateManager (state/create-state-tracker)))
 

CMR Graph system management API.

(ns cmr.graph.system.core
  (:require
    [cmr.graph.system.impl.management :as management]
    [cmr.graph.system.impl.state :as state])
  (:import
    (cmr.graph.system.impl.management StateManager)
    (cmr.graph.system.impl.state StateTracker)))

System State API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defprotocol StateTrackerAPI
  (get-state [this])
  (set-state [this new-state])
  (get-status [this])
  (set-status [this value])
  (get-system [this])
  (set-system [this value])
  (get-system-ns [this])
  (set-system-ns [this value]))
(extend StateTracker
        StateTrackerAPI
        state/behaviour)

State Management API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defprotocol StateManagementAPI
  (init [this] [this mode])
  (deinit [this])
  (start [this] [this mode])
  (stop [this])
  (restart [this] [this mode])
  (startup [this] [this mode])
  (shutdown [this]))
(extend StateManager
        StateManagementAPI
        management/behaviour)
(def create-state-manager #'management/create-state-manager)
 
(ns cmr.graph.components.neo4j
  (:require
   [clojurewerkz.neocons.rest :as nr]
   [cmr.graph.components.config :as config]
   [com.stuartsierra.component :as component]
   [taoensso.timbre :as log]))

Config Component API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn get-conn
  [system]
  (get-in system [:neo4j :conn]))

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

(defrecord Neo4j [conn])
(nr/connect "http://localhost:7474/db/data/")
(defn start
  [this]
  (log/info "Starting Neo4j component ...")
  (let [conn (nr/connect (format "http://%s:%s%s"
                                 (config/neo4j-host this)
                                 (config/neo4j-port this)
                                 (config/neo4j-db-path this)))]
    (log/debug "Started Neo4j component.")
    (assoc this :conn conn)))
(defn stop
  [this]
  (log/info "Stopping Neo4j component ...")
  (log/debug "Stopped Neo4j component.")
  (assoc this :conn nil))
(def lifecycle-behaviour
  {:start start
   :stop stop})
(extend Neo4j
  component/Lifecycle
  lifecycle-behaviour)

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

(defn create-component
  []
  (map->Neo4j {}))
 
(ns cmr.graph.components.core
  (:require
    [cmr.graph.components.config :as config]
    [cmr.graph.components.elastic :as elastic]
    [cmr.graph.components.httpd :as httpd]
    [cmr.graph.components.logging :as logging]
    [cmr.graph.components.neo4j :as neo4j]
    [com.stuartsierra.component :as component]))

Common Configuration Components ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def cfg
  {:config (config/create-component)})
(def log
  {:logging (component/using
             (logging/create-component)
             [:config])})
(def neo4j
  {:neo4j (component/using
           (neo4j/create-component)
           [:config :logging])})
(def elastic
  {:elastic (component/using
             (elastic/create-component)
             [:config :logging])})
(def httpd
  {:httpd (component/using
           (httpd/create-component)
           [:config :logging :neo4j :elastic])})

Component Initializations ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn initialize-bare-bones
  []
  (component/map->SystemMap
    (merge cfg
           log
           neo4j)))
(defn initialize-with-web
  []
  (component/map->SystemMap
    (merge cfg
           log
           neo4j
           elastic
           httpd)))
(def init-lookup
  {:basic #'initialize-bare-bones
   :web #'initialize-with-web})
(defn init
  ([]
    (init :web))
  ([mode]
    ((mode init-lookup))))
 
(ns cmr.graph.components.logging
  (:require
    [clojusc.twig :as logger]
    [com.stuartsierra.component :as component]
    [cmr.graph.components.config :as config]
    [taoensso.timbre :as log]))

Logging Component API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

TBD

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)
    (logger/set-level! log-nss log-level)
    (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.graph.components.elastic
  (:require
   [clojurewerkz.elastisch.rest :as esr]
   [cmr.graph.components.config :as config]
   [com.stuartsierra.component :as component]
   [taoensso.timbre :as log]))

Config Component API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

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

(defrecord Elastic [conn])
(defn start
  [this]
  (log/info "Starting Elasticsearch component ...")
  (let [conn-mgr (clj-http.conn-mgr/make-reusable-conn-manager
                  {:timeout (config/elastic-timeout this)})
        conn (esr/connect (format "http://%s:%s"
                                  (config/elastic-host this)
                                  (config/elastic-port this))
                          {:connection-manager conn-mgr})]
    (log/debug "Started Elasticsearch component.")
    (assoc this :conn conn)))
(defn stop
  [this]
  (log/info "Stopping Elasticsearch component ...")
  (log/debug "Stopped Elasticsearch component.")
  (assoc this :conn nil))
(def lifecycle-behaviour
  {:start start
   :stop stop})
(extend Elastic
  component/Lifecycle
  lifecycle-behaviour)

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

(defn create-component
  []
  (map->Elastic {}))
 
(ns cmr.graph.components.config
  (:require
   [cmr.graph.config :as config]
   [com.stuartsierra.component :as component]
   [taoensso.timbre :as log]))

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

(defn- get-cfg
  [system]
  (get-in system [:config :data]))

Config Component API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn elastic-host
  [system]
  (get-in (get-cfg system) [:elastic :host]))
(defn elastic-port
  [system]
  (get-in (get-cfg system) [:elastic :port]))
(defn elastic-timeout
  [system]
  (get-in (get-cfg system) [:elastic :timeout]))
(defn http-port
  [system]
  (get-in (get-cfg system) [:httpd :port]))
(defn http-docroot
  [system]
  (get-in (get-cfg system) [:httpd :docroot]))
(defn log-level
  [system]
  (get-in (get-cfg system) [:logging :level]))
(defn log-nss
  [system]
  (get-in (get-cfg system) [:logging :nss]))
(defn neo4j-host
  [system]
  (get-in (get-cfg system) [:neo4j :host]))
(defn neo4j-port
  [system]
  (get-in (get-cfg system) [:neo4j :port]))
(defn neo4j-db-path
  [system]
  (get-in (get-cfg system) [:neo4j :db-path]))

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.")
  (assoc this :data nil))
(def lifecycle-behaviour
  {:start start
   :stop stop})
(extend Config
  component/Lifecycle
  lifecycle-behaviour)

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

(defn create-component
  []
  (map->Config {}))
 
(ns cmr.graph.components.httpd
  (:require
    [com.stuartsierra.component :as component]
    [cmr.graph.components.config :as config]
    [cmr.graph.rest.app :as rest-api]
    [org.httpkit.server :as server]
    [taoensso.timbre :as log]))

HTTP Server Component API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

TBD

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

(defrecord HTTPD [])
(defn start
  [this]
  (log/info "Starting httpd component ...")
  (let [port (config/http-port this)
        server (server/run-server (rest-api/app 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.graph.health
  (:require
   [clj-http.client :as httpc]))
(defn http-ok?
  [url]
  (if (= 200 (:status (httpc/head url)))
    true
    false))
(defn has-data?
  [x]
  (if (nil? x)
    false
    true))
(defn config-ok?
  [component]
  (has-data? (:config component)))
(defn elastic-ok?
  [component]
  (http-ok? (get-in component [:elastic :conn :uri])))
(defn logging-ok?
  [component]
  (has-data? (:logging component)))
(defn neo4j-ok?
  [component]
  (http-ok? (get-in component [:neo4j :conn :endpoint :uri])))
(defn components-ok?
  [component]
  {:config {:ok? (config-ok? component)}
   :httpd {:ok? true}
   :elastic {:ok? (elastic-ok? component)}
   :logging {:ok? (logging-ok? component)}
   :neo4j {:ok? (neo4j-ok? component)}})
 
(ns cmr.graph.collections.core
  (:require
   [cheshire.core :as json]
   [clojurewerkz.neocons.rest.cypher :as cypher]
   [clojurewerkz.neocons.rest.nodes :as nodes]
   [clojurewerkz.neocons.rest.relationships :as relations]
   [cmr.graph.queries.neo4j.collections :as query]))
(defn reset
  [conn]
  (cypher/tquery conn query/reset))
(defn get-all
  [conn]
  (cypher/tquery conn query/get-all))
(defn delete-all
  [conn]
  (cypher/tquery conn query/delete-all))
(defn delete-all-cascade
  [conn]
  (cypher/tquery conn query/delete-all-cascade))
(defn batch-add
  [conn ^String json])
(defn add-collection
  [conn ^String json])
(defn get-collection
  [conn ^String concept-id])
(defn delete-collection
  [conn ^String concept-id])
(defn update-collection
  [conn ^String concept-id])
(defn get-collections-via-related-urls
  [conn ^String concept-id]
  (cypher/tquery conn (query/get-urls-by-concept-id concept-id)))
(defn get-concept-ids-by-urls
  [conn urls]
  (cypher/tquery conn (query/get-concept-ids-by-urls urls)))
 

Functions for converting a collection into neo4j create statements.

(ns cmr.graph.data.statement
  (:require
   [clojure.string :as string]
   [digest :as digest]))
(defn- coll-concept-id-key
  [coll]
  (string/replace (first (:concept-id coll)) "-" ))
(defn- coll->provider-node
  [coll]
  (let [provider-id (first (:provider-id coll))]
    [(format "CREATE (%s:Provider {ShortName:'%s'})"
             provider-id provider-id)]))
(defn- coll->coll-stmts
  [coll]
  (let [{:keys [provider-id concept-id entry-id version-id]} coll
        provider-id (first provider-id)
        concept-id (first concept-id)
        entry-id (first entry-id)
        version-id (first version-id)]
    [(format "CREATE (%s:Collection {conceptId: '%s', entryId:'%s', version:'%s'})"
             (coll-concept-id-key coll) concept-id entry-id version-id)
     (format "CREATE (%s)-[:PROVIDES {type:['collection']}]->(%s)"
             provider-id (coll-concept-id-key coll))]))
(defn- url-type->stmt
  [url-type coll-key]
  (let [url-type-key (str "A" (digest/md5 url-type))]
  [(format "CREATE (%s:UrlType {Value:'%s'})"
           url-type-key url-type)
   (format "CREATE (%s)-[:LINKS {type:['collection']}]->(%s)"
           url-type-key coll-key)]))
(defn- url->stmt
  [url coll-key]
  (let [url-key (str "A" (digest/md5 url))]
    [(format "CREATE (%s:Url {name:'%s'})"
             url-key url)
     (format "CREATE (%s)-[:LINKS_TO]->(%s)"
             coll-key url-key)
     (format "CREATE (%s)-[:IS_IN]->(%s)"
             url-key coll-key)]))
(defn- coll->related-urls-stmts
  [coll]
  (let [{:keys [related-urls]} coll
        url-types (distinct (mapv :type related-urls))
        urls (distinct (mapv :url related-urls))
        coll-key (coll-concept-id-key coll)]
    (concat (mapcat #(url-type->stmt % coll-key) url-types)
            (mapcat #(url->stmt % coll-key) urls))))

Returns the neo4j create statements for the given collection

(defn coll->create-stmts
  [coll]
  (concat
   (coll->provider-node coll)
   (coll->coll-stmts coll)
   (coll->related-urls-stmts coll)))

Returns the neo4j statements for building the graph of the given collections.

(defn neo4j-statements
  [colls]
  (string/join "\n" (distinct (mapcat coll->create-stmts colls))))
 

Functions for working with tags data.

(ns cmr.graph.data.tags
  (:require
   [clojure.data.codec.base64 :as b64]
   [clojure.edn :as edn])
  (:import
   (java.util.zip GZIPInputStream)
   (java.io ByteArrayInputStream)))

Converts a base64 encoded gzipped string to EDN.

(defn gzip-base64-tag->edn
  [^String input]
  (-> input
      .getBytes
      b64/decode
      ByteArrayInputStream.
      GZIPInputStream.
      slurp
      edn/read-string))
(comment
 (gzip-base64-tag->edn "H4sIAAAAAAAAAIWPMQvCMBCF/8qRScEGxM3tiAELtSltFXEpoQkhII0ktR1K/7spio4Od8O7997HTcR5Q1vtAh2NDYG2o22p8bJ73nWgD+8UgWnewF/fYJX20btXspdAcqyQ/M11sreDbqz6BRme+DXZAcvE+QCYx+GlqEQGBZZ1yjIO7IglspqX6Q3rVOSwYlggW8Nl+0WaiAzv3SzFSeu8/rxi3BDJQdJ4VTYs6vwC7Me2uQkBAAA="))
 

Functions for importing data into neo4j.

(ns cmr.graph.data.import
  (:require
   [cheshire.core :as json]
   [clojure.data.csv :as csv]
   [clojure.java.io :as io]
   [clojurewerkz.neocons.rest.cypher :as cypher]
   [cmr.graph.data.statement :as statement]
   [cmr.graph.data.tags :as tags]
   [digest :as digest]))
(def json-collections-filename
  "data/all_public_collections_from_es.json")
(def test-file
  "data/testfile.json")
(def collection-csv-file
  "data/collections.csv")
(def collection-url-csv-file
  "data/collection_and_urls.csv")
(def collection-data-center-csv-file
  "data/collection_and_data_centers.csv")
(def collection-tag-csv-file
  "data/collection_and_tags.csv")

List of fields we are interested in parsing from a given URL.

(def url-fields
  [:type :url])

List of fields to parse from a collection record.

(def relevant-fields
  [:concept-id :provider-id :entry-id :related-urls :data-center :version-id :metadata-format
   :tags-gzip-b64])

Parses a single URL field into all the nodes we want to create for the URL.

(defn parse-url-into-nodes
  [url]
  (select-keys (json/parse-string url true) url-fields))

Returns each of the tags and associated data from the provided Elasticsearch tags-gzip-b64 field.

(defn- parse-tags
  [tags-gzip-b64]
  (when tags-gzip-b64
    (tags/gzip-base64-tag->edn tags-gzip-b64)))

When a hash just isn't good enough.

(defn md5-leo
  [value]
  (str "A" (digest/md5 value)))

Returns only the relevant JSON fields from the provided collection record for import into neo4j.

(defn prepare-collection-for-import
  [collection]
  (update (select-keys (:fields collection) relevant-fields)
          :related-urls
          (fn [urls]
            (mapv parse-url-into-nodes urls))))

Reads a JSON file into memory

(defn read-json-file
  [filename]
  (json/parse-string (slurp (io/resource filename)) true))

Columns in the collections CSV file.

(def collection-columns
  ["MD5Leo" "ConceptId" "ProviderId" "VersionId" "MetadataFormat"])

Returns a row to write to the collections CSV file for a given collection.

(defn collection->row
  [collection]
  (let [{:keys [provider-id concept-id version-id metadata-format]} collection]
    [(md5-leo (first concept-id))
     (first concept-id)
     (first provider-id)
     (first version-id)
     (first metadata-format)]))

Creates the collection csv file

(defn write-collection-csv
  [collections output-filename]
  (with-open [csv-file (io/writer output-filename)]
    (csv/write-csv csv-file [collection-columns])
    (csv/write-csv csv-file (mapv collection->row collections))))

Creates a collection URL row for a relationship CSV file.

(defn construct-collection-url-row
  [collection url]
  [(md5-leo (first (:concept-id collection)))
   (:url url)
   (:type url)])

Creates a collection data center row for a relationship CSV file.

(defn construct-collection-data-center-row
  [collection data-center]
  [(md5-leo (first (:concept-id collection)))
   data-center])

Creates the collection<->url relationship csv file.

(defn write-collection-url-relationship-csv
  [collections output-filename]
  (let [rows (doall
              (for [collection collections
                    url (:related-urls collection)]
                (construct-collection-url-row collection url)))]
    (with-open [csv-file (io/writer output-filename)]
      (csv/write-csv csv-file [["CollectionMD5Leo" "URL" "URLType"]])
      (csv/write-csv csv-file rows))))

Creates the collection<->data centers relationship csv file.

(defn write-collection-data-center-relationship-csv
  [collections output-filename]
  (let [rows (doall
              (for [collection collections
                    data-center (:data-center collection)]
                (construct-collection-data-center-row collection data-center)))]
    (with-open [csv-file (io/writer output-filename)]
      (csv/write-csv csv-file [["CollectionMD5Leo" "DataCenter"]])
      (csv/write-csv csv-file rows))))

Creates a collection data center row for a relationship CSV file.

(defn- construct-collection-tag-row
  [collection tag]
  (let [[tag-key tag-association-data] tag]
    [(md5-leo (first (:concept-id collection)))
     tag-key]))

Creates the collection<->tag relationship csv file.

(defn write-collection-tags-relationship-csv
  [collections output-filename]
  (let [rows (doall
              (for [collection collections
                    tag (parse-tags (first (:tags-gzip-b64 collection)))]
                (construct-collection-tag-row collection tag)))]
    (with-open [csv-file (io/writer output-filename)]
      (csv/write-csv csv-file [["CollectionMD5Leo" "TagKey"]])
      (csv/write-csv csv-file rows))))

All of the import statements to run to populate a completely empty database. Make sure to delete everything before running.

(def import-statements
  ["CREATE CONSTRAINT ON (url:Url) ASSERT url.name IS UNIQUE"
   "CREATE CONSTRAINT ON (urlType:UrlType) ASSERT urlType.name IS UNIQUE"
   "CREATE CONSTRAINT ON (coll:Collection) ASSERT coll.md5Leo IS UNIQUE"
   "CREATE CONSTRAINT ON (dataCenter:DataCenter) ASSERT dataCenter.name IS UNIQUE"
   "CREATE CONSTRAINT ON (tag:Tag) ASSERT tag.name IS UNIQUE"
   "LOAD CSV WITH HEADERS FROM \"https://raw.githubusercontent.com/cmr-exchange/cmr-graph/master/resources/data/collections.csv\" AS csvLine
      MERGE (format:MetadataFormat {name: csvLine.MetadataFormat})
      MERGE (version:Version {name: csvLine.VersionId})
      MERGE (provider:Provider {name: csvLine.ProviderId})
      CREATE (coll:Collection {md5Leo: csvLine.MD5Leo, conceptId: csvLine.ConceptId})
      CREATE (coll)-[:OWNED_BY]->(provider)
      CREATE (coll)-[:FORMATTED_IN]->(format)
      CREATE (coll)-[:VERSION_IS]->(version)"
   "USING PERIODIC COMMIT 500
      LOAD CSV WITH HEADERS FROM \"https://raw.githubusercontent.com/cmr-exchange/cmr-graph/master/resources/data/collection_and_urls.csv\" AS csvLine
      MATCH (coll:Collection { md5Leo: csvLine.CollectionMD5Leo})
      MERGE (url:Url { name: csvLine.URL})
      MERGE (urlType:UrlType { name: csvLine.URLType})
      CREATE (coll)-[:LINKS_TO]->(url)
      CREATE (url)-[:HAS_TYPE]->(urlType)"
   "USING PERIODIC COMMIT 500
      LOAD CSV WITH HEADERS FROM \"https://raw.githubusercontent.com/cmr-exchange/cmr-graph/master/resources/data/collection_and_data_centers.csv\" AS csvLine
      MATCH (coll:Collection { md5Leo: csvLine.CollectionMD5Leo})
      MERGE (dataCenter:DataCenter { name: csvLine.DataCenter})
      CREATE (coll)-[:AFFILIATED_WITH]->(dataCenter)"
   "USING PERIODIC COMMIT 500
      LOAD CSV WITH HEADERS FROM \"https://raw.githubusercontent.com/cmr-exchange/cmr-graph/master/resources/data/collection_and_tags.csv\" AS csvLine
      MATCH (coll:Collection { md5Leo: csvLine.CollectionMD5Leo})
      MERGE (tag:Tag { name: csvLine.TagKey})
      CREATE (coll)-[:TAGGED_WITH]->(tag)"])

Imports all of the collection data.

(defn import-all-data
  [conn]
  (doseq [statement import-statements]
    (cypher/tquery conn statement)))
(comment
 (prepare-collection-for-import (first (:hits (:hits (read-json-file json-collections-filename)))))
 (mapv prepare-collection-for-import (:hits (:hits (read-json-file test-file))))
 (prepare-collection-for-import (first (:hits (:hits (read-json-file json-collections-filename)))))

 (write-collection-csv (mapv prepare-collection-for-import (:hits (:hits (read-json-file json-collections-filename))))
                       (str "resources/" collection-csv-file))

 (write-collection-url-relationship-csv (mapv prepare-collection-for-import (:hits (:hits (read-json-file json-collections-filename))))
                                        (str "resources/" collection-url-csv-file))

 (write-collection-data-center-relationship-csv (mapv prepare-collection-for-import (:hits (:hits (read-json-file json-collections-filename))))
                                                (str "resources/" collection-data-center-csv-file))

 (write-collection-tags-relationship-csv (mapv prepare-collection-for-import (:hits (:hits (read-json-file json-collections-filename))))
                                         (str "resources/" collection-tag-csv-file))

 (mapv prepare-collection-for-import (:hits (:hits (read-json-file test-file))))
 (println
  (statement/neo4j-statements (mapv prepare-collection-for-import (:hits (:hits (read-json-file test-file)))))))
 
(ns cmr.graph.rest.route
  (:require
   [cmr.graph.components.config :as config]
   [cmr.graph.components.neo4j :as neo4j]
   [cmr.graph.health :as health]
   [cmr.graph.rest.handler :as handler]
   [reitit.ring :as ring]
   [taoensso.timbre :as log]))

CMR Graph Database Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn collections
  [httpd-component]
  (let [conn (neo4j/get-conn httpd-component)]
    [["/collections"
      {:get (handler/get-collections conn)
       :delete (handler/delete-collections conn)
       :post (handler/add-collections conn)
       :options handler/ok}]
     ["/collections/import"
      {:post (handler/import-collection-data conn)
       :options handler/ok}]
     ["/collection"
      {:post (handler/add-collection conn)
       :options handler/ok}]
     ["/collection/:concept-id"
      {:get (handler/get-collection conn)
       :delete (handler/delete-collection conn)
       :put (handler/update-collection conn)
       :options handler/ok}]]))
(defn relationships
  [httpd-component]
  (let [conn (neo4j/get-conn httpd-component)]
    [["/relationships/related-urls/collections/:concept-id"
      {:get (handler/get-collections-via-related-urls conn)
       :options handler/ok}]]))

CMR Elasticsearch Graph Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Demo Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn movie-demo
  [httpd-component]
  (let [conn (neo4j/get-conn httpd-component)]
    [["/demo/movie/graph/:limit" {
      :get (handler/movie-demo-graph conn)
      :options handler/ok}]
     ["/demo/movie/search" {
      :get (handler/movie-demo-search conn)
      :options handler/ok}]
     ["/demo/movie/title/:title" {
      :get (handler/movie-demo-title conn)
      :options handler/ok}]]))

Static Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn static
  [httpd-component]
  (let [docroot (config/http-docroot httpd-component)]
    [["/static/*" {
      :get (handler/static-files docroot)}]]))

Admin Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn admin
  [httpd-component]
  (let [conn (neo4j/get-conn httpd-component)]
    [["/health" {
                 :get (handler/health httpd-component)
                 :options handler/ok}]
     ["/reset" {
                :delete (handler/reset conn)
                :options handler/ok}]
     ["/reload" {:post (handler/reload conn)
                 :options handler/ok}]
     ["/ping" {
               :get handler/ping
               :post handler/ping
               :options handler/ok}]]))

DANGEROUS!!! REMOVE ME!!! *Injection Routes ;;;;;;;;;;;;;;;;;;;;;;;;;

(defn dangerous
  [httpd-component]
  (let [conn (neo4j/get-conn httpd-component)]
    [["/queries/cypher"
      {:get (handler/cypher-injection-get conn)
       :post (handler/cypher-injection-post conn)
       :options handler/ok}]]))

Utility Routes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

 

Custom ring middleware for CMR Graph.

(ns cmr.graph.rest.middleware
  (:require
   [cmr.graph.rest.response :as response]
   [taoensso.timbre :as log]))
(defn wrap-cors
  [handler]
  (fn [request]
    (response/cors request (handler request))))
 
(ns cmr.graph.rest.app
  (:require
   [clojure.java.io :as io]
   [cmr.graph.rest.handler :as handler]
   [cmr.graph.rest.middleware :as middleware]
   [cmr.graph.rest.route :as route]
   [ring.middleware.defaults :as ring-defaults]
   [reitit.ring :as ring]
   [taoensso.timbre :as log]))
(defn rest-api-routes
  [httpd-component]
  (concat
   (route/collections httpd-component)
   (route/relationships httpd-component)
   (route/static httpd-component)
   (route/movie-demo httpd-component)
   (route/admin httpd-component)
   (route/dangerous httpd-component)))
(defn app
  [httpd-component]
  (-> httpd-component
      rest-api-routes
      ring/router
      (ring/ring-handler handler/fallback)
      (ring-defaults/wrap-defaults ring-defaults/api-defaults)
      (middleware/wrap-cors)))
 
(ns cmr.graph.rest.handler
  (:require
   [clojure.java.io :as io]
   [clojurewerkz.neocons.rest.cypher :as cypher]
   [cmr.graph.data.import :as data-import]
   [clojusc.twig :as twig]
   [cmr.graph.collections.core :as collections]
   [cmr.graph.demo.movie :as movie]
   [cmr.graph.health :as health]
   [cmr.graph.rest.response :as response]
   [ring.middleware.file :as file-middleware]
   [ring.util.codec :as codec]
   [taoensso.timbre :as log]))

Graph Handlers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn get-collections
  [conn]
  (fn [request]
    (->> conn
         (collections/get-all)
         (response/json request))))
(defn delete-collections
  [conn]
  (fn [request]
    (let [cascade? (get-in request [:params :cascade])]
      (if (= "true" cascade?)
        (->> conn
             (collections/delete-all-cascade)
             (response/json request))
        (->> conn
             (collections/delete-all)
             (response/json request))))))

Expects the body to be a JSON payload of an array of node objects.

(defn add-collections
  [conn]
  (fn [request]
    (->> request
         :body
         slurp
         ;(collections/batch-add conn)
         ((fn [_] {:error :not-implemented}))
         (response/json request))))

Expects the body to be a JSON payload of a node object.

(defn add-collection
  [conn]
  (fn [request]
    (->> request
         :body
         slurp
         ;(collections/add-collection conn)
         ((fn [_] {:error :not-implemented}))
         (response/json request))))
(defn get-collection
  [conn]
  (fn [request]
    (->> [:path-params :concept-id]
         (get-in request)
         ;(collections/get-collection conn)
         ((fn [_] {:error :not-implemented}))
         (response/json request))))
(defn delete-collection
  [conn]
  (fn [request]
    (->> [:path-params :concept-id]
         (get-in request)
         ;(collections/delete-collection conn)
         ((fn [_] {:error :not-implemented}))
         (response/json request))))
(defn update-collection
  [conn]
  (fn [request]
    (->> [:path-params :concept-id]
         (get-in request)
         ;(collections/update-collection conn)
         ((fn [_] {:error :not-implemented}))
         (response/json request))))
(defn- get-related-urls
  [conn concept-id]
  (let [result (collections/get-collections-via-related-urls conn concept-id)]
    (map #(get % "u.name") result)))
(defn get-collections-via-related-urls
  [conn]
  (fn [request]
    (let [related-urls (get-related-urls
                        conn
                        (get-in request [:path-params :concept-id]))
          result (collections/get-concept-ids-by-urls conn related-urls)
          concept-ids (distinct (map #(get % "c.conceptId") result))]
      (response/json request concept-ids))))

Imports all of our collection data.

(defn import-collection-data
  [conn]
  (fn [request]
    (data-import/import-all-data conn)
    (response/ok request)))

Demo Handlers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn movie-demo-graph
  [conn]
  (fn [request]
    (->> [:path-params :limit]
         (get-in request)
         Integer.
         (movie/get-graph conn)
         (response/json request))))
(defn movie-demo-search
  [conn]
  (fn [request]
    (->> [:params :q]
         (get-in request)
         (movie/search conn)
         (response/json request))))
(defn movie-demo-title
  [conn]
  (fn [request]
    (->> [:path-params :title]
         (get-in request)
         (codec/percent-decode)
         (movie/get-movie conn)
         (response/json request))))

Admin Handlers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn health
  [component]
  (fn [request]
    (->> component
         health/components-ok?
         (response/json request))))
(defn reset
  [conn]
  (fn [request]
    ;; delete things in small increments to avoid hanging the system
    (collections/delete-all-cascade conn)
    (collections/reset conn)
    (response/ok request)))
(defn reload
  [conn]
  (fn [request]
    ;; delete things in small increments to avoid hanging the system
    (collections/delete-all-cascade conn)
    (collections/reset conn)
    (data-import/import-all-data conn)
    (response/ok request)))
(def ping
  (fn [request]
    (response/json request {:result :pong})))

DANGEROUS!!! REMOVE ME!!! *Injection Handlers ;;;;;;;;;;;;;;;;;;;;;;;

Call with something like this:

$ curl http://localhost:3012/queries/cypher?q='MATCH%20(people:Person)%20RETURN%20people.name%20LIMIT%2010;'

But don't, really. Since we're going to delete this :-)

(defn cypher-injection-get
  [conn]
  (fn [request]
    (->> [:params :q]
         (get-in request)
         (codec/percent-decode)
         (cypher/tquery conn)
         (response/json request))))

Call with something like this:

$ curl -XPOST -H 'Content-Type: text/plain' http://localhost:3012/queries/cypher -d 'MATCH (people:Person) RETURN people.name LIMIT 10;'

But don't, really. Since we're going to delete this :-)

(defn cypher-injection-post
  [conn]
  (fn [request]
    (->> request
         :body
         slurp
         (cypher/tquery conn)
         (response/json request))))

Utility Handlers ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def ok
  (fn [request]
    (response/ok request)))
(def fallback
  (fn [request]
    (response/not-found request)))
(defn static-files
  [docroot]
  (fn [request]
    (if-let [doc-resource (.getPath (io/resource docroot))]
      (file-middleware/file-request request doc-resource))))
 
(ns cmr.graph.rest.response
  (:require
   [cheshire.core :as json]
   [ring.util.http-response :as response]
   [taoensso.timbre :as log]))
(defn ok
  [_request & args]
  (response/ok args))
(defn json
  [_request data]
  (-> data
      json/generate-string
      response/ok
      (response/content-type "application/json")))
(defn text
  [_request data]
  (-> data
      response/ok
      (response/content-type "text/plain")))
(defn not-found
  [_request]
  (response/content-type
   (response/not-found "Not Found")
   "plain/text"))
(defn cors
  [request response]
  (case (:request-method request)
    :options (-> response
                 (response/content-type "text/plain; charset=utf-8")
                 (response/header "Access-Control-Allow-Origin" "*")
                 (response/header "Access-Control-Allow-Methods" "POST, PUT, GET, DELETE, OPTIONS")
                 (response/header "Access-Control-Allow-Headers" "Content-Type")
                 (response/header "Access-Control-Max-Age" "2592000"))
    (response/header response "Access-Control-Allow-Origin" "*")))
 
(ns cmr.graph.config
  (:require
   [clojure.edn :as edn]
   [clojure.java.io :as io]))
(def config-file "config/cmr-graph/config.edn")
(defn data
  ([]
    (data config-file))
  ([filename]
    (with-open [rdr (io/reader (io/resource filename))]
      (edn/read (new java.io.PushbackReader rdr)))))