r2r-designer

0.1


backend for the r2r-designer

dependencies

org.clojure/clojure
1.6.0
org.clojure/java.jdbc
0.3.3
org.clojure/data.json
0.2.4
com.stuartsierra/component
0.2.1
compojure
1.1.8
ring/ring-core
1.3.2
ring-server
0.4.0
ring-cors
0.1.4
ring/ring-json
0.3.1
clj-http
0.9.2
edu.ucdenver.ccp/kr-sesame-core
1.4.17
com.taoensso/timbre
3.2.1
org.postgresql/postgresql
9.3-1102-jdbc41
com.zaxxer/HikariCP-java6
2.2.5
org.aksw.sparqlify/sparqlify-core
0.6.12
org.clojure/data.csv
0.1.2



(this space intentionally left almost blank)
 

0.1

(defproject r2r-designer 
  :description "backend for the r2r-designer"
  :url "http://rdanitz.github.io/r2r-designer/"
  :license { :name "MIT"
             :url "http://opensource.org/licenses/MIT" }
  :repositories [["aksw-internal" "http://maven.aksw.org/repository/internal/"]]
  :dependencies [
    [org.clojure/clojure "1.6.0"]
    [org.clojure/java.jdbc "0.3.3"]
    [org.clojure/data.json "0.2.4"]
    [com.stuartsierra/component "0.2.1"]
    [compojure "1.1.8"]
    [ring/ring-core "1.3.2"]
    [ring-server "0.4.0"]
    [ring-cors "0.1.4"]
    [ring/ring-json "0.3.1"]
    [clj-http "0.9.2"]
    [edu.ucdenver.ccp/kr-sesame-core "1.4.17"]
    [com.taoensso/timbre "3.2.1"]
    [org.postgresql/postgresql "9.3-1102-jdbc41"]
    [com.zaxxer/HikariCP-java6 "2.2.5"]
    [org.aksw.sparqlify/sparqlify-core "0.6.12" :exclusions [[postgresql/postgresql]]]
    [org.clojure/data.csv "0.1.2"] 
    ]
  :plugins [;; for standalone server: 
            ;;'lein ring uberjar'
            ;;'java -jar target/r2r-designer-{version}-standalone.jar
            [lein-ring "0.9.1"]
            ;; invoke with 'lein marg -f server.html .'
            [lein-marginalia "0.8.0"]] 
  :ring {:init system/init
         :destroy system/destroy
         :handler system/app}
  :source-paths ["server/src"]
  :test-paths ["server/test"]
  :resource-paths ["server/resources"]
  :main system
  :profiles {
    :dev {
      :dependencies [
        [org.clojure/tools.namespace "0.2.5"]
        [org.clojure/java.classpath "0.2.2"]
        [ring-mock "0.1.5"]
        [ring/ring-devel "1.3.2"]
        ]
      :source-paths ["server/dev"]
      :resource-paths [".tmp" "app"]
      }
    :uberjar {
      :aot [system]
      :main system 
      }
    })
 

Namespace 'user' will always be executed. Call '(dev)' to get things started.

(defn dev []
  (require 'dev)
  (in-ns 'dev))
 
(do (clojure.core/ns system.main (:gen-class)) (clojure.core/defn -main [] ((do (clojure.core/require (quote ring.server.leiningen)) (clojure.core/resolve (quote ring.server.leiningen/serve))) (quote {:ring {:auto-reload? false, :stacktraces? false, :open-browser? false, :destroy system/destroy, :init system/init, :handler system/app}}))))
 

Every separate module within the backend is organized as a component; every component consists of a list of dependencies to other components, and a lifecycle to create, start and stop the component.

(ns components)
 

Encapsulates a database connection, as well as CSV file management.

(ns components.datasource
  (:require
    [taoensso.timbre :as timbre]
    [com.stuartsierra.component :as c]
    [core.db :refer [new-pool]]))
(timbre/refer-timbre)
(defrecord Datasource [spec pool csv-file]
  c/Lifecycle
  (start [c]
    (info "starting datasource adapter ...")
    (reset! (:pool c) (new-pool c))
    c)
  (stop [c]
    (info "stopping datasource adapter ...")
    (when (:pool c) 
      (if @(:pool c) (.close @(:pool c)))
      (reset! (:pool c) nil))
    (reset! (:csv-file c) nil)
    (reset! (:separator c) nil)
    c))

Creates a new datasource component. Consists of: 'spec': holds the db params, 'pool': connection pool for the db, 'csv-file': current active file descriptor, 'separator': according csv file separator, 'max-pool': number of connections.

(defn new-datasource 
  [opts]
  (map->Datasource {:spec (atom (select-keys opts [:driver :host :name :username :password]))
                    :pool (atom nil)
                    :csv-file (atom nil)
                    :separator (atom nil)
                    :max-pool 10}))
 

Logging component.

(ns components.logging
  (:require
    [com.stuartsierra.component :as c]
    [taoensso.timbre :as timbre]))
(timbre/refer-timbre)
(defrecord Logger [config]
  c/Lifecycle
  (start [component]
    (info "start logging ...")
    (timbre/merge-config! config)
    component)
  (stop [component]
    (info "stop logging ...")
    component))
(defn new-logger [config]
  (map->Logger {:config config}))
 

Encapsulates an OpenRDF connection.

(ns components.openrdf
  (:require
    [com.stuartsierra.component :as c]
    [taoensso.timbre :as timbre]
    [core.openrdf :refer :all])
  (:import
    org.openrdf.repository.http.HTTPRepository))
(timbre/refer-timbre)
(defrecord OpenRDF []
  c/Lifecycle
  (start [c]
    (info "starting openrdf ...")
    (reset! (:server c) (HTTPRepository. (:host c) (:repo c)))
    c) 
  (stop [c]
    (info "stopping openrdf ...")
    (reset! (:server c) nil)
    c))
(defn new-openrdf [opts]
  (map->OpenRDF {:server (atom nil)
                 :host (:host opts)
                 :repo (:repo opts)
                 :base-uri (:base-uri opts)
                 }))
 

Encapsulates the Suggestion API (LOV or LinDA oracle).

(ns components.oracle
  (:require
    [com.stuartsierra.component :as c]
    [taoensso.timbre :as timbre]
    [core.oracle :refer :all]))
(timbre/refer-timbre)
(defrecord Oracle [kb endpoint sample threshold limit n]
  c/Lifecycle
  (start [component]
    (info "starting oracle ...")
    (reset! (:kb component) 
            (new-server (:endpoint component)))
    component) 
  (stop [component]
    (info "stopping oracle ...")
    (reset! (:kb component) nil)
    component))

Creates a new Oracle adapter with endpoint.

(defn new-oracle 
  [endpoint]
  (map->Oracle {:endpoint endpoint
                ;; knowledge base
                :kb (atom nil)
                ;; number of data points to regard
                :sample 20
                ;; min score for suggestions
                :threshold 0.4
                ;; number of suggestions
                :limit 20
                :n 5}))
 

Encapsulates the Ring web server holding the web app.

(ns components.ring
  (:require
    [com.stuartsierra.component :as c]
    [taoensso.timbre :as timbre]
    [routes.app :refer [app-fn]]
    [ring.server.standalone :refer [serve]]))
(timbre/refer-timbre)
(defrecord Ring [server app-fn opts db-api oracle-api]
  c/Lifecycle
  (start [component] 
    (info "starting ring adapter ...")
    (let [app (app-fn component)]
      (reset! server (serve app opts))
      component))
  (stop [component] 
    (info "stopping ring adapter ...") 
    (if @server
      (.stop @server)
      (reset! server nil))
    component))

Creates a new Ring adapter, holding the server and all api pathes.

(defn new-ring 
  [app-fn opts]
  (map->Ring {:opts opts 
              :server (atom nil) 
              :app-fn app-fn
              :db-api "/api/v1/db"
              :csv-api "/api/v1/csv"
              :oracle-api "/api/v1/oracle"
              :transform-api "/api/v1/transform"
              :file-store (atom {})}))
 

Encapsulates the Sparqlify SPARQL-SQL-Rewriter engine.

(ns components.sparqlify
  (:require
    [com.stuartsierra.component :as c]
    [taoensso.timbre :as timbre]
    [core.sparqlify :refer :all]))
(timbre/refer-timbre)
(defrecord Sparqlify [host port server]
  c/Lifecycle
  (start [c]
    (info "starting sparqlify ...")
    (when (:server c) 
      (if @(:server c) (.start @(:server c))))
    c) 
  (stop [c]
    (info "stopping sparqlify ...")
    (when (:server c) 
      (if @(:server c) (.stop @(:server c)))
      (reset! (:server c) nil))
    c))
(defn new-sparqlify [opts]
  (map->Sparqlify {:host (:host opts)
                   :port (:port opts)
                   :server (atom nil)}))
 

The 'core' namespace carries the implementation of the main functionality. It also provides purely functional API wrappers to external libs and services.

(ns core)
 

Core CSV functionality.

(ns core.csv
  (:require
    [com.stuartsierra.component :as c]
    [taoensso.timbre :as timbre]
    [clojure.java.io :as io]
    [clojure.data.csv :as csv]
    [clojure.string :as str]))
(timbre/refer-timbre)

guesses (stupidly) the separator used in a csv file

(defn separator 
  [file]
  (let [first-line (re-find #".*[\n\r]" (slurp file))
        tabs (count (filter #(= \tab %) first-line))
        commata (count (filter #(= \, %) first-line))]
    (if (> tabs commata)
      \tab
      \,)))

sets a new CSV file and its separator

(defn set-file 
  [c file]
  (reset! (:csv-file c) file)
  (reset! (:separator c) (separator file)))

retrieves the first 10 data items of the saved csv file

(defn get-data 
  [c]
  (let [file @(:csv-file c)
        separator @(:separator c)]
    (if file
      (with-open [r (io/reader file)] (doall (take 10 (csv/read-csv r :separator separator))))
      [])))
 

Core SQL database (PostgreSQL) functionality.

(ns core.db
  (:require
    [com.stuartsierra.component :as c]
    [taoensso.timbre :as timbre]
    [clojure.java.io :as io]
    [clojure.java.jdbc :as jdbc]
    [clojure.string :as str])
  (:import
    [java.sql DriverManager]
    [com.zaxxer.hikari HikariDataSource HikariConfig]))
(timbre/refer-timbre)

creates a new Hikari connection pool

(defn new-pool 
  [c]
  (let [max-pool (:max-pool c)
        spec @(:spec c)
        ds (HikariDataSource.)]
    (doto ds 
      (.setMaximumPoolSize max-pool)
      (.setConnectionTestQuery "SELECT 1")
      (.setDataSourceClassName (:driver spec))
      (.addDataSourceProperty "serverName" (:host spec))
      (.addDataSourceProperty "databaseName" (:name spec))
      (.addDataSourceProperty "user" (:username spec))
      (.addDataSourceProperty "password" (:password spec)))
    ds))

tries to connect to a given database via spec

(defn test-db 
  [spec]
  (let [host (:host spec)
        name (:name spec)
        user (:username spec)
        pass (:password spec)
        connection-str (str "jdbc:postgresql://" host "/" name)]
    (try 
      (let [conn (DriverManager/getConnection connection-str user pass)]
        (.close conn)
        true)
      (catch Exception e false))))

setse a new database configuration as active

(defn register-db 
  [db new-spec]
  (info "registering new data source")
  (if db (c/stop db))
  (reset! (:spec db) new-spec)
  (c/start db))

returns all db tables

(defn get-tables 
  [c]
  (let [ds {:datasource @(:pool c)}
        result (jdbc/query ds "SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = 'public'")
        tables (map :table_name result)]
    (debug tables)
    tables))

returns all columns for table

(defn get-columns
  [c table]
  (let [ds {:datasource @(:pool c)}
        result (jdbc/query ds (str "SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = '" table "'"))
        columns (map :column_name result)]
    (debug columns)
    columns))

returns a map with tables mapped to their columns

(defn get-table-columns 
  [c]
  (let [ds {:datasource @(:pool c)}
        tables (get-tables c)
        table-columns (apply merge (for [table tables] {table (get-columns c table)}))]
    table-columns))

returns a column

(defn query-column 
  [c table column]
  (let [ds {:datasource @(:pool c)}
        result (jdbc/query ds (str "SELECT \"" column "\" FROM " table))
        values (map second (map first result))]
    (debug values)
    values))
 

core OpenRDF functions

(ns core.openrdf
  (:require
    [com.stuartsierra.component :as c]
    [taoensso.timbre :as timbre])
  (:import
    org.openrdf.rio.RDFFormat
    org.openrdf.model.Resource))
(timbre/refer-timbre)

uploads a N3 triple dump to OpenRDF

(defn upload! 
  [c f]
  (let [repo @(:server c)
        uri (:base-uri c)]
    (with-open [conn (.getConnection repo)]
      (.add conn f uri RDFFormat/NTRIPLES (into-array Resource [])))
    (str (:host c) "/" (:repo c))))
 

naïve implementation of the Oracle RDF suggestion engine

(ns core.oracle
  (:require
    [clojure.data.json :as json]
    [clojure.string :refer [join split]]
    [clj-http.client :as client]
    [edu.ucdenver.ccp.kr.kb :refer :all]
    [edu.ucdenver.ccp.kr.rdf :refer :all]
    [edu.ucdenver.ccp.kr.sparql :refer :all]
    [edu.ucdenver.ccp.kr.sesame.kb :as sesame]
    [taoensso.timbre :as timbre]
    [core.db :as db]))
(timbre/refer-timbre)

returns a sesame server wrapping the given endpoint

(defn new-server 
  [endpoint]
  (open 
    (sesame/new-sesame-server
      :server endpoint
      :repo-name "lov")))

adds RDF namespaces to a knowledge base

(defn add-namespaces 
  [kb]
  (update-namespaces kb
   '(("rdf" "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
     ("rdfs" "http://www.w3.org/2000/01/rdf-schema#")
     ("dcterms" "http://purl.org/dc/terms/")
     ("skos" "http://www.w3.org/2004/02/skos/core#"))))

These are utility functions to retrieve several useful information about an RDF entity:

tries to find a label for the RDF entity

(defn get-label
  [c entity]
  (let [kb (add-namespaces @(:kb c))]
    (let [result (sparql-query kb (format "select distinct * where { <%s> <http://www.w3.org/2000/01/rdf-schema#label> ?x. } limit 1" entity))]
      (map '?/x result))))

tries to find a comment for the RDF entity

(defn get-comment 
  [c entity]
  (let [kb (add-namespaces @(:kb c))]
    (let [result (sparql-query kb (format "select distinct * where { <%s> <http://www.w3.org/2000/01/rdf-schema#comment> ?x. } limit 1" entity))]
      (map '?/x result))))

tries to find the title for the RDF entity

(defn get-title 
  [c entity]
  (let [kb (add-namespaces @(:kb c))]
    (let [result (sparql-query kb (format "select distinct * where { <%s> <http://purl.org/dc/terms/title> ?x. } limit 1" entity))]
      (map '?/x result))))

tries to find a description for the RDF entity

(defn get-description 
  [c entity]
  (let [kb (add-namespaces @(:kb c))]
    (let [result1 (sparql-query kb (format "select distinct * where { <%s> <http://purl.org/dc/terms/description> ?x. } limit 1" entity))
          entity2 (if (and entity (.endsWith entity "#")) (subs entity 0 (dec (count entity)))) 
          result2 (if entity2 (sparql-query kb (format "select distinct * where { <%s> <http://purl.org/dc/terms/description> ?x. } limit 1" entity2)) [])
          merged (concat result1 result2)]
      (map '?/x merged))))

tries to find a definition for the RDF entity

(defn get-definition 
  [c entity]
  (let [kb (add-namespaces @(:kb c))]
    (let [result (sparql-query kb (format "select distinct * where { <%s> <http://www.w3.org/2004/02/skos/core#definition> ?x. } limit 1" entity))]
      (map '?/x result))))

returns all hits with score > .4

(defn significant 
  [c results]
  (filter #(> (:score %) (:threshold c)) results))

takes the first n matches

(defn cut 
  [c results]
  (take (:n c) results))

filters unneccesary information from the matches

(defn shaped 
  [results]
  (let [filter-fn #(select-keys % [:score :uri :prefixedName :vocabulary.prefix])
        filtered (map filter-fn results)]
    filtered))

adds a localName and the vocabulary URI to the matches

(defn transform 
  [entity-map]
  (let [prefixedName (first (:prefixedName entity-map))
        vocabPrefix (first (:vocabulary.prefix entity-map))
        localName (if (and prefixedName vocabPrefix) (subs prefixedName (inc (.length vocabPrefix))))
        uri (first (:uri entity-map))
        vocab (if (and uri localName) (subs uri 0 (- (.length uri) (.length localName))))]
    (assoc entity-map
           :localName (if localName [localName] [])
           :vocabulary.uri (if vocab [vocab] []))))

adds additional useful information like comments, descriptions, definition, etc.

(defn enrich 
  [c entity-map]
  (let [uri (first (:uri entity-map))
        vocab (first (:vocabulary.uri entity-map))]
    (assoc entity-map 
           :label (get-label c uri)
           :comment (get-comment c uri)
           :definition (get-definition c uri)
           :vocabulary.title (get-title c vocab)
           :vocabulary.description (get-description c vocab))))

combines all specified transformations (inner and outer transformation)

(defn process
  [c raw]
  (let [;; transformation on the whole result set
        outer (->> raw 
                  (significant c)
                  (cut c)
                  shaped)
        ;; transformation on individual matches
        inner (map (comp #(enrich c %) transform) outer)]
    inner))

queries the Linked Open Vocabulary search API; type is either 'class' or 'property'

(defn query-lov
  [c type query]
  (if (and c query (seq query))
    (let [api "http://lov.okfn.org/dataset/lov/api/v2/search"
          results (-> (client/get api {:query-params {:q query :type type}}) :body json/read-json :results)]
      results)
    []))

splits an expr to several items

(defn expr->items 
  [expr] 
  (filter seq (split expr #"[\s,;]+")))

searches for classes in LOV

(defn search-classes 
  [c expr]
  (process c (apply concat (map #(query-lov c "class" %) (expr->items expr)))))

searches for properties in LOV

(defn search-properties 
  [c expr]
  (process c (apply concat (map #(query-lov c "property" %) (expr->items expr)))))

preferrably returns a tag for a given table, if existing; otherwise returns the name of the table

(defn item-or-tag 
  [item tag] (if (empty? tag) item tag))

returns suggestions for a given table and column; database has to be configured

(defn recommend 
  [c table columns]
  (let [t-rec {:name (:name table) 
               :recommend (search-classes c (item-or-tag (:name table) (:tag table)))}
        c-rec (for [i columns] 
                {:name (:name i) 
                 :recommend (search-properties c (item-or-tag (:name i) (:tag i)))})]
    {:table t-rec :columns c-rec}))
 

This is a clean wrapper around the Sparqlify functions, that are needed. Essentially, these are (dumber) re-implementations of the specific Main methods.

(ns core.sparqlify
  (:require
    [com.stuartsierra.component :as c]
    [clojure.java.shell :as sh]
    [clojure.java.io :as io]
    [taoensso.timbre :as timbre])
  (:import 
    java.io.File
    org.apache.commons.cli.GnuParser
    org.apache.commons.cli.Options
    org.apache.jena.riot.out.NTriplesWriter
    org.aksw.jena_sparql_api.core.utils.QueryExecutionUtils
    ;; org.aksw.sparqlify.core.RdfViewSystemOld
    org.aksw.sparqlify.util.SparqlifyUtils
    org.aksw.sparqlify.web.SparqlifyCliHelper
    org.aksw.sparqlify.web.SparqlFormatterUtils
    org.aksw.sparqlify.csv.CsvParserConfig
    org.aksw.sparqlify.csv.CsvMapperCliMain
    org.aksw.sparqlify.csv.InputSupplierCSVReader ))
(timbre/refer-timbre)

writes the mapping as string to a temporary file

(defn mapping-to-file 
  [mapping]
  (let [mapping-file (if mapping (File/createTempFile "mapping" ".sml"))] 
    (when mapping-file 
      (spit mapping-file mapping))
    mapping-file))

needed? (defn init-sparqlify! [] (RdfViewSystemOld/initSparqlifyFunctions))

pretends to be called as from the command line; returns a CLI config object

pseudo-parse pseudo-options into config [...]

(defn mapping-to-config 
  [mapping-file]
  (let [options (doto (Options.) (.addOption "m" "mapping" true ""))
        parser (GnuParser.)
        cmd (.parse parser options (into-array String ["-m" mapping-file]))
        config (SparqlifyCliHelper/parseSmlConfigs cmd nil)]
    config))

returns a configured Sparqlify engine

sadly cannot easily kill sub-processes when invoking jar's through shell but ... common, seriously guys? rough rewrite of sparqlify-core's main; most of the config is not needed? some vars in main are never used

(defn config-sparqlify
  [c mapping-file]
  ;; (init-sparqlify!)
  (let [pool @(:pool (:datasource c))
        config (mapping-to-config mapping-file)]
    (SparqlifyUtils/createDefaultSparqlifyEngine pool config nil nil)))

exposes a Sparqlify endpoint for a given mapping

(defn start-sparql-endpoint! 
  [sparqlify mapping-file]
  (let [port (:port sparqlify)
        qef (config-sparqlify sparqlify mapping-file)
        server (org.aksw.sparqlify.web.Main/createSparqlEndpoint qef port)]
    (info "publishing new SPARQL endpoint")
    (if sparqlify (c/stop sparqlify))
    (reset! (:server sparqlify) server)
    (c/start sparqlify)
    (str (:host sparqlify) ":" (:port sparqlify) "/sparql")))

dumps the Sparqlify endpoint regarding the given mapping; returns a temp file containing the dump as N3 triples.

(defn sparqlify-dump 
  [c mapping-file]
  (let [qef (config-sparqlify c mapping-file)
        it (QueryExecutionUtils/createIteratorDumpTriples qef)
        f (File/createTempFile "dump" ".nt")]
    (with-open [out (io/output-stream f)] 
      (NTriplesWriter/write out it))
    f))

wrapper to sparqlify-csv command line tool; returns a temp file containing the dump as N3 triples.

(defn sparqlify-csv 
  [c mapping-file]
  (let [csv-file @(:csv-file (:datasource c))]
    (with-open [in (io/input-stream mapping-file)]
      (let [template-config (CsvMapperCliMain/readTemplateConfig in nil)
            view (first (.getDefinitions template-config)) ; pick only(?) view
            f (File/createTempFile "dump" ".nt")
            csv-config (doto (CsvParserConfig.) (.setFieldSeparator @(:separator (:datasource c))))
            csv-reader (InputSupplierCSVReader. csv-file csv-config)
            results (CsvMapperCliMain/createResultSetFromCsv csv-reader true (int 100))
            it (CsvMapperCliMain/createTripleIterator results view)]
        (with-open [out (io/output-stream f)]
          (SparqlFormatterUtils/writeText out it))
        f))))
 

Tools for interactive development with the REPL. This file should not be included in a production build of the application.

(ns dev
  (:require
    ;; external namespaces
    [clojure.java.io :as io]
    [clojure.java.shell :as sh]
    [clojure.java.javadoc :refer (javadoc)]
    [clojure.java.jdbc :as jdbc]
    [clojure.pprint :refer (pprint)]
    [clojure.reflect :refer (reflect)]
    [clojure.repl :refer (apropos dir doc find-doc pst source)]
    [clojure.string :as string]
    [clojure.test :as test]
    [clojure.tools.namespace.repl :refer (refresh refresh-all)]
    [clojure.tools.reader.edn :as edn]
    [clojure.set :refer :all]
    [clojure.java.classpath :refer :all]
    [clojure.data.json :as json]
    [clojure.data.csv :as data-csv]
    [com.stuartsierra.component :as c]
    [ring.server.standalone :refer :all]
    [ring.middleware.file-info :refer :all]
    [ring.middleware.file :refer :all]
    [clj-http.client :as client]
    [taoensso.timbre :as timbre]
    [edu.ucdenver.ccp.kr.kb :as kb]
    [edu.ucdenver.ccp.kr.rdf :as rdf]
    [edu.ucdenver.ccp.kr.sparql :as sparql]
    [edu.ucdenver.ccp.kr.sesame.kb :as sesame]
    ;; internal namespaces
    [components.datasource :refer :all]
    [components.oracle :refer :all]
    [components.sparqlify :refer :all]
    [components.openrdf :refer :all]
    [components.ring :refer :all]
    [core.db :refer :all]
    [core.csv :as csv]
    [core.oracle :refer :all]
    [core.sparqlify :refer :all]
    [core.openrdf :refer :all]
    [routes.app :refer [app-fn]]
    [system :refer [new-system]]))
(timbre/refer-timbre)

The #'system var holds the system during development in the REPL.

A Var containing an object representing the application under development.

(def system
  nil)
(def log-config {:ns-whitelist []
                 :ns-blacklist []})

Creates and initializes the system under development in the Var #'system.

(defn init
  []
  (let [db-opts {:driver "org.postgresql.ds.PGSimpleDataSource"
                 :host "localhost"
                 :name "mydb"
                 :username "postgres"
                 :password ""
                 }
        ring-opts {:port 3000
                   :open-browser? false
                   :join true
                   :auto-reload? true}
        oracle-sparql-endpoint "http://localhost:8080/openrdf-sesame"
        sparqlify-opts {:host "http://localhost"
                        :port 7531}
        openrdf-opts {:host "http://localhost:8080/openrdf-sesame"
                      :repo "r2r"
                      :base-uri "http://mycompany.com"}]
    (alter-var-root #'system (constantly (new-system db-opts #'app-fn ring-opts oracle-sparql-endpoint log-config sparqlify-opts openrdf-opts)))))

Starts the system running, updates the Var #'system.

(defn start
  []
  (alter-var-root #'system c/start))

Stops the system if it is currently running, updates the Var #'system.

(defn stop
  []
  (alter-var-root #'system
    (fn [s] (when s (c/stop s))))
  :stopped)

Initializes and starts the system running.

(defn go
  []
  (init)
  (start)
  :ready)

Stops the system, reloads modified source files, and restarts it.

(defn reset
  []
  (stop)
  (refresh :after 'dev/go))
 

The 'routes' namespaces defines the backend API of the r2r-designer. You can find the specification within /server/resource/public/api-docs.raml.

(ns routes
  (:require 
    [ring.util.response :refer [response]]))

Implements Cross-Origin-Resource-Sharing by adding a corresponding header.

(defn preflight 
  [request]
  (assoc
    (response "CORS enabled")
    :headers {"Access-Control-Allow-Origin" "*" 
              "Access-Control-Allow-Methods" "PUT, DELETE, POST, GET, OPTIONS, XMODIFY" 
              "Access-Control-Max-Age" "2520"
              "Access-Control-Allow-Credentials" "true"
              "Access-Control-Request-Headers" "x-requested-with, content-type, origin, accept"
              "Access-Control-Allow-Content-Type" "*" 
              "Access-Control-Allow-Headers" "x-requested-with, content-type, origin, accept"}))
 

This configures and holds the web app with all its routes.

(ns routes.app
  (:require
    [clojure.stacktrace :as st]
    [taoensso.timbre :as timbre]
    [ring.middleware.cors :refer :all]
    [ring.middleware.params :refer :all]
    [ring.middleware.multipart-params :refer :all]
    [ring.middleware.json :refer :all]
    [ring.middleware.file-info :refer :all]
    [compojure.core :refer :all]
    [compojure.handler :as handler]
    [compojure.route :as route]
    [routes.db :refer [db-routes-fn]]
    [routes.csv :refer [csv-routes-fn]]
    [routes.oracle :refer [oracle-routes-fn]]
    [routes.transform :refer [transform-routes-fn]]))
(timbre/refer-timbre)

some default routes

(defroutes app-routes
  (route/resources "/" {:root "public"})
  (route/not-found "Not Found!"))

CORS headers

(defn allow-origin [handler]
  (fn [request]
    (let [response (handler request)
          headers (:headers response)]
      (assoc response :headers (assoc headers "Access-Control-Allow-Origin" "*")))))
(defn allow-content-type [handler]
  (fn [request]
    (let [response (handler request)
          headers (:headers response)]
      (assoc response :headers (assoc headers "Access-Control-Allow-Content-Type" "*")))))
(defn wrap-dir-index [handler]
  (fn [req]
    (handler
     (update-in req [:uri]
                #(if (= "/" %) "/index.html" %)))))

middleware to monitor incoming requests and outgoing responses

(defn monitor 
  [handler]
  (fn [{:keys [request-method uri query-string] :as request}]
    (let [response (handler request)]
      (debug request)
      (info request-method uri query-string)
      (let [{:keys [status body]} response]
        (info status body)
        (debug response)
        response))))

catches every non-catched exceptions and wraps it into a 500 with a corresponding fault string

(defn wrap-exception 
  [f]
  (fn [request]
    (try (f request)
      (catch Exception e
        (let [stacktrace (with-out-str (st/print-stack-trace e))]
          {:status 500
           :body stacktrace})))))

creates an app dynamically according to the current system

(defn app-fn 
  [component]
  ;; 'routes' bundles all routes which constitutes the API
  (-> (routes (db-routes-fn component)
              (csv-routes-fn component)
              (oracle-routes-fn component) 
              (transform-routes-fn component) 
              app-routes)
      wrap-params
      wrap-multipart-params
      (wrap-json-body {:keywords? true})
      (wrap-json-response {:pretty true})
      wrap-exception
      allow-content-type
      allow-origin
      wrap-dir-index
      wrap-file-info
      monitor))
 
(ns routes.csv
  (:require 
    [compojure.core :refer :all]
    [taoensso.timbre :as timbre]
    [ring.util.codec :as codec]
    [core.csv :refer :all]
    [clojure.data.json :refer [write-str]]
    [routes :refer [preflight]]))  
(timbre/refer-timbre)
(defn csv-routes-fn [component]
  (let [api (:csv-api component)
        ds (:datasource component)]
    (defroutes csv-routes
      (GET (str api "/data") [] 
           (try 
             (write-str (get-data ds))
             (catch Exception _ {:status 500})))
      (OPTIONS (str api "/upload") request (preflight request))
      (POST (str api "/upload") request
        (let [file (-> request :multipart-params (get "file") :tempfile)]
          (try 
            (do
              (set-file ds file)
              {:status 200 :body "uploaded"}) 
            (catch Exception _ {:status 400 :body "error"})))))))
 
(ns routes.db
  (:require 
    [compojure.core :refer :all]
    [taoensso.timbre :as timbre]
    [ring.util.codec :as codec]
    [core.db :refer :all]
    [clojure.data.json :refer [write-str]]
    [routes :refer [preflight]]))  
(timbre/refer-timbre)
(defn db-routes-fn [component]
  (let [api (:db-api component)
        db (:datasource component)]
    (defroutes db-routes
      (GET (str api "/tables") [] 
           (try 
             (write-str (get-tables db))
             (catch Exception _ {:status 500})))
      (GET (str api "/table-columns") [] 
           (try 
             (write-str (get-table-columns db))
             (catch Exception _ {:status 500})))
      (GET (str api "/column") [table name :as r] (write-str (query-column db table name)))
      (OPTIONS (str api "/test") request (preflight request))
      (POST (str api "/test") [driver host name username password :as r]
        (let [spec {:driver driver
                    :host host
                    :name name 
                    :username username
                    :password password}
              result (test-db spec)]
          {:status 200 :body (str result)}))
      ; TODO: password is sent in plain text!
      (OPTIONS (str api "/register") request (preflight request))
      (POST (str api "/register") [driver host name username password :as r]
        (let [db (:datasource component)
              new-spec {:host host
                        :driver driver
                        :name name 
                        :username username
                        :password password}]
          (register-db db new-spec)
          {:status 200})))))
 
(ns routes.oracle
  (:require 
    [compojure.core :refer :all]
    [taoensso.timbre :as timbre]
    [ring.util.response :refer [response]]
    [core.oracle :refer :all]
    [routes :refer [preflight]]))
(timbre/refer-timbre)
(defn oracle-routes-fn [c]
  (let [api (:oracle-api c)]
    (defroutes oracle-routes
      (OPTIONS api request (preflight request))
      (POST api request 
        (let [oracle (:oracle c)
              data (:body request)
              suggestions (recommend oracle (:table data) (:columns data))]
          (response suggestions))))))
 
(ns routes.transform
  (:require 
    [compojure.core :refer :all]
    [taoensso.timbre :as timbre]
    [clojure.set :refer :all]
    [ring.util.codec :as codec]
    [ring.util.response :refer [response file-response]]
    [clojure.java.io :as io]
    [clojure.data.json :as json]
    [core.db :as db]
    [core.sparqlify :refer :all]
    [core.openrdf :refer :all]
    [routes :refer [preflight]])
  (:import java.io.File)) 
(timbre/refer-timbre)
(defn transform-routes-fn [c]
  (let [api (:transform-api c)]
    (defroutes transform-routes
      (OPTIONS (str api "/dump-db") r (preflight r))
      (POST (str api "/dump-db") r
        (let [sparqlify (:sparqlify c)
              file-store (:file-store c) 
              mapping (spy (:mapping (:body r)))
              f (spy (mapping-to-file mapping))
              dump-file (spy (sparqlify-dump sparqlify (str f)))
              -hash (spy (hash dump-file))]
          (swap! file-store (fn [x] (assoc x -hash dump-file))) 
          {:status 200 :body (str (:transformApi c) "/file/" -hash ".nt")}))
      (OPTIONS (str api "/dump-csv") r (preflight r))
      (POST (str api "/dump-csv") r
        (let [sparqlify (:sparqlify c)
              file-store (:file-store c) 
              mapping (:mapping (:body r))
              f (spy (mapping-to-file mapping))
              dump-file (spy (sparqlify-csv sparqlify (str f)))
              -hash (hash dump-file)]
          (swap! file-store (fn [x] (assoc x -hash dump-file))) 
          {:status 200 :body (str (:transformApi c) "/file/" -hash ".nt")}))
      (OPTIONS (str api "/publish/openrdf") r (preflight r))
      (POST (str api "/publish/openrdf") r 
        (let [sparqlify (:sparqlify c)
              openrdf (:openrdf c)
              file-store (:file-store c) 
              mapping (:mapping (:body r))
              f (mapping-to-file mapping)
              dump-file (sparqlify-csv sparqlify (str f))
              endpoint (upload! openrdf dump-file)]
          {:status 200 :body endpoint}))
      (OPTIONS (str api "/publish/sparqlify") r (preflight r))
      (POST (str api "/publish/sparqlify") r 
        (let [sparqlify (:sparqlify c)
              mapping (:mapping (:body r))
              f (mapping-to-file mapping)
              endpoint (start-sparql-endpoint! sparqlify (str f))]
          {:status 200 :body endpoint}))
      ;; TODO: possible access to arbitrary files on the system through known filenames?
      (GET (str api "/file/:id") [id]
        (let [-hash (second (re-find #"(.*)\.nt" id))
              sparqlify (:sparqlify c)
              file-store (:file-store c)
              f (get @file-store (Integer. -hash))]
          (if f (file-response (str f))))))))
 

This holds the system configuration at runtime. Every state is encapsuled in the #'system var.

(ns system
  (:require
    [taoensso.timbre :as timbre]
    [com.stuartsierra.component :as c]
    [components.datasource :refer :all]
    [components.oracle :refer :all]
    [components.sparqlify :refer :all]
    [components.openrdf :refer :all]
    [components.ring :refer :all]
    [components.logging :refer :all]
    [routes.app :refer [app-fn]])
  (:gen-class))
(timbre/refer-timbre)

Creates a new system configuration as pure data.

(defn new-system 
  [db-opts app-fn ring-opts oracle-sparql-endpoint log-config sparqlify-opts openrdf-opts] 
  (c/system-map
    :log (c/using (new-logger log-config) [])
    :datasource (c/using (new-datasource db-opts) [:log])
    :oracle (c/using (new-oracle oracle-sparql-endpoint) [:datasource :log])
    :sparqlify (c/using (new-sparqlify sparqlify-opts) [:datasource :log])
    :openrdf (c/using (new-openrdf openrdf-opts) [:log])
    :ring (c/using (new-ring app-fn ring-opts) [:datasource :oracle :sparqlify :openrdf :log])))

configuration options

logging

(def log-config {:ns-whitelist [] 
                 :ns-blacklist []})

initial database configuration

(def db-opts {:driver  
              :host  
              :name  
              :username  
              :password }) 

ring web server options

(def ring-opts {:port 3000 
                :open-browser? false 
                :join true 
                :auto-reload? true})

SPARQL endpoint for additional information from the oracle. As of now, the Linked Open Vocabulary SPARQL endpoint has been discontinued; use /resources/lov.n3 to populate a private OpenRDF repository or use another one.

(def oracle-sparql-endpoint "http://localhost:8080/openrdf-sesame")

Where to open up the SPARQL endpoint?

(def sparqlify-opts {:host "http://localhost"
                     :port 7531}) 

Where to dump to the created data?

(def openrdf-opts {:host "http://localhost:8080/openrdf-sesame"
                   :repo "r2r"
                   :base-uri "http://mycompany.com"})

Configure new system and start it. When running as a jar, the init fn has to be called by hand.

(defn -main []
  (info "calling system/-main")
  (let [system (new-system 
                    db-opts 
                    #'app-fn 
                    ring-opts 
                    oracle-sparql-endpoint 
                    log-config 
                    sparqlify-opts
                    openrdf-opts)]
    (c/start system)))