<commit_before>(ns uruk.core
  "Marklogic XCC core functions: session management, querying, type
  conversion, transactions."
  (:require [clojure.set]
            [clojure.data.json :as json]
            [clojure.data.xml :as xml]
            [slingshot.slingshot :as sling])
  (:import [java.util.logging Logger]
           [com.marklogic.xcc
            Session$TransactionMode
            RequestOptions
            ContentCreateOptions ContentPermission ContentCapability
            ContentSourceFactory ContentFactory
            DocumentFormat DocumentRepairLevel
            ValueFactory]
           [com.marklogic.xcc.types ValueType]
           [com.marklogic.xcc Version]
           java.net.URI))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Enumerations and classes.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def version
  "XCC release number. Auto-generated.

  See https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/Version.html"
  {:string (Version/getVersionString)
   :version-major (Version/getVersionMajor)
   :version-minor (Version/getVersionMinor)
   :version-patch (Version/getVersionPatch)})

(def doc-format
  "Enumeration of allowed document formats. Used at insertion
  time.
  See https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/DocumentFormat.html"
  {:xml DocumentFormat/XML
   :json   DocumentFormat/JSON
   :text   DocumentFormat/TEXT
   :none   DocumentFormat/NONE
   :binary DocumentFormat/BINARY})

(def doc-repair-level
  "Enumeration of document repair levels. Used at insertion time.
  See https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/DocumentRepairLevel.html"
  {:default DocumentRepairLevel/DEFAULT
   :full    DocumentRepairLevel/FULL
   :none    DocumentRepairLevel/NONE})

(def content-capability
  "Enumeration of content permission capability values.
  See https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/ContentCapability.html"
  {:execute ContentCapability/EXECUTE
   :insert  ContentCapability/INSERT
   :read    ContentCapability/READ
   :update  ContentCapability/UPDATE})

(def transaction-modes
  "Enumeration of valid Session transaction modes. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/Session.TransactionMode.html"
  {:auto Session$TransactionMode/AUTO
   :query Session$TransactionMode/QUERY
   :update Session$TransactionMode/UPDATE
   :update-auto-commit Session$TransactionMode/UPDATE_AUTO_COMMIT})


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Type conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn convert-number
  "Reads a number from a numeric Java object of a type from
  com.marklogic.xcc.types. Returns nil if not a number. Designed for
  robust number-handling while preventing read-string security
  issues. Regex from http://stackoverflow.com/a/12285023/706499. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/types/package-summary.html"
  [obj]
  (let [s (.asString obj)]
    (when (re-find #"^-?\d+\.?\d*$" s)
      (clojure.edn/read-string s))))

(defn java-json->clj-json
  "Given a MarkLogic XCC JSON object, returns a Clojure representation
  of its JSON data."
  [java-json]
  (json/read-str (.toString (.asJsonNode java-json))))

(defn xdm-var->str
  "Returns a String representation of the given XDM variable"
  [xdm-var]
  (hash-map (.toString (.getName xdm-var))
            (.toString (.getValue xdm-var))))

(def types
  "Default mapping from MarkLogic XCC types (e.g. those that might be
  returned in a query's result set) to Clojure functions intended to
  convert to Clojure types. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/types/package-summary.html"
  {"array-node()" java-json->clj-json ;; ArrayNode
   "boolean-node()" #(.asBoolean %)  ;; BooleanNode

   "cts:box" #(.asString %) ;; would be nice to convert box to seq of 4 numbers, unless that is disrupted by other element types
   "cts:circle" #(.asString %)
   "cts:point" #(.asString %)
   "cts:polygon" #(.asString %)

   "json:array" java-json->clj-json ;; JSArray
   "json:object" java-json->clj-json ;; JSObject

   ;; JsonItem TODO
   "null-node()" java-json->clj-json ;; NullNode
   "number-node()" convert-number ;; NumberNode
   "object-node()" java-json->clj-json ;; ObjectNode

   ;; XdmAtomic TODO
   "attribute()" #(.asString %) ;; XdmAttribute
   "binary()" #(.asBinaryData %) ;; XdmBinary
   "comment()" #(.asString %) ;; XdmComment
   "document-node()" #(.asString %) ;; XdmDocument
   "duration()" #(.toString %) ;; XdmDuration XXX may only ever come through as xs:duration
   "element()" #(.asString %) ;; XdmElement
   ;; XdmItem TODO
   "text()" #(.asString %) ;; XdmText
   "variable()" xdm-var->str ;; XdmVariable -- XXX unknown if the key used matches getValueType

   "xs:anyURI" str
   "xs:base64Binary" #(.asBinaryData %) ;; XSBase64Binary
   "xs:boolean" #(.asPrimitiveBoolean %)
   "xs:date" str
   "xs:dateTime" str
   "xs:dayTimeDuration" str
   "xs:decimal" convert-number
   "xs:double" convert-number
   "xs:duration" str
   "xs:float" convert-number
   ;; Maybe strip hyphens from all Gregorian date-parts?
   ;; or make conform to a particular date type?
   "xs:gDay" str
   "xs:gMonth" str
   "xs:gMonthDay" str
   "xs:gYear" str
   "xs:gYearMonth" str
   "xs:hexBinary" str ;; looks OK but test with real doc
   "xs:integer" convert-number
   "xs:QName" str
   "xs:string" str
   "xs:time" str
   "xs:untypedAtomic" str ;; NB: also referred to as "xdt:untypedAtomic" in type listing
   "xs:yearMonthDuration" str})

(defn result-type
  "Returns type string of the given query Result object. Currently
  assumes result is homogenous."
  [result]
  (.toString (.getValueType (first (.toArray result)))))

(defn convert-types
  "Return the result of applying type conversion to the given
  MarkLogic query result sequence. Default type mappings can be
  overridden (in part or in whole) with the optional parameter
  `type-mapping`, which should contain a transformation function keyed
  by an XCC type string. See `types` above."
  [result-sequence & [type-mapping]]
  ;; TODO throw informative exception if type not found in types
  (map (fn [item] (((merge types
                          (when (map? type-mapping)
                            type-mapping))
                   (.toString (.getValueType item))) item))
       (.toArray result-sequence)))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Helpers for sessions and requests
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def valid-request-options
  "Set of valid request options for Request objects. Can also be
  passed to Sessions as :default-request-options. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/RequestOptions.html"
  #{:auto-retry-delay-millis
    :cache-result
    :default-xquery-version
    :effective-point-in-time
    :locale
    :max-auto-retry
    :query-language
    :request-name
    :request-time-limit
    :result-buffer-size
    :timeout-millis
    :timezone})

(defn request-options
  "Creates a Request Options object (to pass to a Request or a
  Session) out of the given options map. See `valid-request-options`
  for supported keywords."
  [options]
  (let [request (RequestOptions.)]
    (when-not (every? valid-request-options (keys options))
      ;; TODO switch to spec
      (throw (IllegalArgumentException. "Invalid request option. Keywords passed in `options` must be a subset of `valid-request-options`.")))
    (when-let [ardm (:auto-retry-delay-millis options)]
      (.setAutoRetryDelayMillis request ardm))
    (when (contains? options :cache-result)
      (.setCacheResult request (:cache-result options)))
    (when-let [dxv (:default-xquery-version options)]
      (.setDefaultXQueryVersion request dxv))
    ;; TODO A macro might be useful to replace the chain of when-lets with use-once variable names
    (when-let [epit (:effective-point-in-time options)]
      (.setEffectivePointInTime request (BigInteger. (str epit))))

    (when-let [locale (:locale options)]
      (.setLocale request locale)) ;; TODO enforce Locale object?
    (when-let [mar (:max-auto-retry options)]
      (.setMaxAutoRetry request (Integer. mar)))
    (when-let [ql (:query-language options)]
      (.setQueryLanguage request ql))
    (when-let [rn (:request-name options)]
      (.setRequestName request rn))

    (when-let [rtl (:request-time-limit options)]
      (.setRequestTimeLimit request rtl))
    (when-let [rbs (:result-buffer-size options)]
      (.setResultBufferSize request rbs))
    (when-let [tm (:timeout-millis options)]
      (.setTimeoutMillis request tm))
    (when-let [tz (:timezone options)] ;; TODO enforce type?
      (.setTimeZone request tz))
    ;; TODO check types of above
    ;; TODO test each
    request))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Session management
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- create-session*
  "Creates session, given map of database info."
  [{:keys [uri user password content-base]}]
  (let [cs (ContentSourceFactory/newContentSource (URI. uri))]
    (cond
      (and (nil? content-base)
           (or (nil? user)
               (nil? password))) (.newSession cs)

      (and (seq content-base)
           (or (nil? user)
               (nil? password))) (.newSession cs content-base)

      (and (seq user)
           (seq password)
           (nil? content-base)) (.newSession cs user password)

      (and (seq user)
           (seq password)
           (seq content-base)) (.newSession cs user password content-base))))

(defn create-session
  "Create a Session for querying and transacting with. Parameter
  `db-info` describing database connection information must
  include :uri key, and may optionally include connection information
  for :content-base (database name), and/or :user and :password.

  See `newSession` methods at
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/ContentSource.html
  for detail on allowed parameter arrangements.

  If optional `options` map is passed, the session is configured
  accordingly. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/Session.html
  for valid options. (Note that request options are distinct from
  session options, though *default* request options can be set for the
  session.)"
  ([db-info]
   (create-session* db-info))
  ([db-info {:keys [default-request-options logger user-object
                    transaction-mode transaction-timeout]}]
   (let [session (create-session* db-info)]
     (when (map? default-request-options)
       (.setDefaultRequestOptions session (request-options default-request-options)))
     (when (instance? Logger logger)
       (.setLogger session logger))
     ;; XXX the following is not the strictest test!
     (when (instance? Object user-object)
       (.setUserObject session user-object))
     (when (keyword? transaction-mode)
       (.setTransactionMode session (transaction-modes transaction-mode)))
     (when (integer? transaction-timeout)
       (.setTransactionTimeout session transaction-timeout))
     session)))

(defn session-options
  "Returns a map describing all options on the given Session object."
  [session]
  {:default-request-options (.getDefaultRequestOptions session)
   :effective-request-options (.getEffectiveRequestOptions session)
   :logger (.getLogger session)
   :xaresource (.getXAResource session)
   :user-object (.getUserObject session)
   :transaction-timeout (.getTransactionTimeout session)
   :transaction-mode ((clojure.set/map-invert transaction-modes)
                      (.getTransactionMode session))})<commit_msg>Additional session creation entry points<commit_after>(ns uruk.core
  "Marklogic XCC core functions: session management, querying, type
  conversion, transactions."
  (:require [clojure.set]
            [clojure.data.json :as json]
            [clojure.data.xml :as xml]
            [slingshot.slingshot :as sling])
  (:import [java.util.logging Logger]
           java.net.URI
           [com.marklogic.xcc
            Session$TransactionMode
            RequestOptions SecurityOptions
            ContentCreateOptions ContentPermission ContentCapability
            ContentSource ContentSourceFactory ContentFactory
            DocumentFormat DocumentRepairLevel
            ValueFactory Version]
           [com.marklogic.xcc.types ValueType]
           [com.marklogic.xcc.spi ConnectionProvider]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Enumerations and classes.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def version
  "XCC release number. Auto-generated.

  See https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/Version.html"
  {:string (Version/getVersionString)
   :version-major (Version/getVersionMajor)
   :version-minor (Version/getVersionMinor)
   :version-patch (Version/getVersionPatch)})

(def doc-format
  "Enumeration of allowed document formats. Used at insertion
  time.
  See https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/DocumentFormat.html"
  {:xml DocumentFormat/XML
   :json   DocumentFormat/JSON
   :text   DocumentFormat/TEXT
   :none   DocumentFormat/NONE
   :binary DocumentFormat/BINARY})

(def doc-repair-level
  "Enumeration of document repair levels. Used at insertion time.
  See https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/DocumentRepairLevel.html"
  {:default DocumentRepairLevel/DEFAULT
   :full    DocumentRepairLevel/FULL
   :none    DocumentRepairLevel/NONE})

(def content-capability
  "Enumeration of content permission capability values.
  See https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/ContentCapability.html"
  {:execute ContentCapability/EXECUTE
   :insert  ContentCapability/INSERT
   :read    ContentCapability/READ
   :update  ContentCapability/UPDATE})

(def transaction-modes
  "Enumeration of valid Session transaction modes. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/Session.TransactionMode.html"
  {:auto Session$TransactionMode/AUTO
   :query Session$TransactionMode/QUERY
   :update Session$TransactionMode/UPDATE
   :update-auto-commit Session$TransactionMode/UPDATE_AUTO_COMMIT})


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Type conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn convert-number
  "Reads a number from a numeric Java object of a type from
  com.marklogic.xcc.types. Returns nil if not a number. Designed for
  robust number-handling while preventing read-string security
  issues. Regex from http://stackoverflow.com/a/12285023/706499. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/types/package-summary.html"
  [obj]
  (let [s (.asString obj)]
    (when (re-find #"^-?\d+\.?\d*$" s)
      (clojure.edn/read-string s))))

(defn java-json->clj-json
  "Given a MarkLogic XCC JSON object, returns a Clojure representation
  of its JSON data."
  [java-json]
  (json/read-str (.toString (.asJsonNode java-json))))

(defn xdm-var->str
  "Returns a String representation of the given XDM variable"
  [xdm-var]
  (hash-map (.toString (.getName xdm-var))
            (.toString (.getValue xdm-var))))

(def types
  "Default mapping from MarkLogic XCC types (e.g. those that might be
  returned in a query's result set) to Clojure functions intended to
  convert to Clojure types. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/types/package-summary.html"
  {"array-node()" java-json->clj-json ;; ArrayNode
   "boolean-node()" #(.asBoolean %)  ;; BooleanNode

   "cts:box" #(.asString %) ;; would be nice to convert box to seq of 4 numbers, unless that is disrupted by other element types
   "cts:circle" #(.asString %)
   "cts:point" #(.asString %)
   "cts:polygon" #(.asString %)

   "json:array" java-json->clj-json ;; JSArray
   "json:object" java-json->clj-json ;; JSObject

   ;; JsonItem TODO
   "null-node()" java-json->clj-json ;; NullNode
   "number-node()" convert-number ;; NumberNode
   "object-node()" java-json->clj-json ;; ObjectNode

   ;; XdmAtomic TODO
   "attribute()" #(.asString %) ;; XdmAttribute
   "binary()" #(.asBinaryData %) ;; XdmBinary
   "comment()" #(.asString %) ;; XdmComment
   "document-node()" #(.asString %) ;; XdmDocument
   "duration()" #(.toString %) ;; XdmDuration XXX may only ever come through as xs:duration
   "element()" #(.asString %) ;; XdmElement
   ;; XdmItem TODO
   "text()" #(.asString %) ;; XdmText
   "variable()" xdm-var->str ;; XdmVariable -- XXX unknown if the key used matches getValueType

   "xs:anyURI" str
   "xs:base64Binary" #(.asBinaryData %) ;; XSBase64Binary
   "xs:boolean" #(.asPrimitiveBoolean %)
   "xs:date" str
   "xs:dateTime" str
   "xs:dayTimeDuration" str
   "xs:decimal" convert-number
   "xs:double" convert-number
   "xs:duration" str
   "xs:float" convert-number
   ;; Maybe strip hyphens from all Gregorian date-parts?
   ;; or make conform to a particular date type?
   "xs:gDay" str
   "xs:gMonth" str
   "xs:gMonthDay" str
   "xs:gYear" str
   "xs:gYearMonth" str
   "xs:hexBinary" str ;; looks OK but test with real doc
   "xs:integer" convert-number
   "xs:QName" str
   "xs:string" str
   "xs:time" str
   "xs:untypedAtomic" str ;; NB: also referred to as "xdt:untypedAtomic" in type listing
   "xs:yearMonthDuration" str})

(defn result-type
  "Returns type string of the given query Result object. Currently
  assumes result is homogenous."
  [result]
  (.toString (.getValueType (first (.toArray result)))))

(defn convert-types
  "Return the result of applying type conversion to the given
  MarkLogic query result sequence. Default type mappings can be
  overridden (in part or in whole) with the optional parameter
  `type-mapping`, which should contain a transformation function keyed
  by an XCC type string. See `types` above."
  [result-sequence & [type-mapping]]
  ;; TODO throw informative exception if type not found in types
  (map (fn [item] (((merge types
                          (when (map? type-mapping)
                            type-mapping))
                   (.toString (.getValueType item))) item))
       (.toArray result-sequence)))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Helpers for sessions and requests
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def valid-request-options
  "Set of valid request options for Request objects. Can also be
  passed to Sessions as :default-request-options. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/RequestOptions.html"
  #{:auto-retry-delay-millis
    :cache-result
    :default-xquery-version
    :effective-point-in-time
    :locale
    :max-auto-retry
    :query-language
    :request-name
    :request-time-limit
    :result-buffer-size
    :timeout-millis
    :timezone})

(defn request-options
  "Creates a Request Options object (to pass to a Request or a
  Session) out of the given options map. See `valid-request-options`
  for supported keywords."
  [options]
  (let [request (RequestOptions.)]
    (when-not (every? valid-request-options (keys options))
      ;; TODO switch to spec
      (throw (IllegalArgumentException. "Invalid request option. Keywords passed in `options` must be a subset of `valid-request-options`.")))
    (when-let [ardm (:auto-retry-delay-millis options)]
      (.setAutoRetryDelayMillis request ardm))
    (when (contains? options :cache-result)
      (.setCacheResult request (:cache-result options)))
    (when-let [dxv (:default-xquery-version options)]
      (.setDefaultXQueryVersion request dxv))
    ;; TODO A macro might be useful to replace the chain of when-lets with use-once variable names
    (when-let [epit (:effective-point-in-time options)]
      (.setEffectivePointInTime request (BigInteger. (str epit))))

    (when-let [locale (:locale options)]
      (.setLocale request locale)) ;; TODO enforce Locale object?
    (when-let [mar (:max-auto-retry options)]
      (.setMaxAutoRetry request (Integer. mar)))
    (when-let [ql (:query-language options)]
      (.setQueryLanguage request ql))
    (when-let [rn (:request-name options)]
      (.setRequestName request rn))

    (when-let [rtl (:request-time-limit options)]
      (.setRequestTimeLimit request rtl))
    (when-let [rbs (:result-buffer-size options)]
      (.setResultBufferSize request rbs))
    (when-let [tm (:timeout-millis options)]
      (.setTimeoutMillis request tm))
    (when-let [tz (:timezone options)] ;; TODO enforce type?
      (.setTimeZone request tz))
    ;; TODO check types of above
    ;; TODO test each
    request))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Session management
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;



(defn security-options
  "TODO"
  ([ssl-context] (security-options ssl-context nil))
  ([ssl-context {:keys [protocols cipher-suites]}]
   (let [sec-opts (SecurityOptions. ssl-context)]
     (when (seq protocols)
       (.setEnabledProtocols sec-opts (into-array String protocols)))
     (when (seq cipher-suites)
       (.setEnabledCipherSuites sec-opts (into-array String cipher-suites)))
     sec-opts)))


(defn uri-content-source
  "TODO"
  ([uri] (uri-content-source uri nil))
  ;; Return a ContentSource object that will serve as the source of
  ;; connections to the server specified by the given URI.
  ([uri security-opts]
   ;; TODO enforce or construct SecurityOptions for security-opts
   (ContentSourceFactory/newContentSource (if (instance? URI uri)
                                            uri (URI. uri))
                                          security-opts)))

(defn hosted-content-source
  "TODO"
  ([host port]
   ;; Return a ContentSource object that will serve as the source of
   ;; connections to the server on the given host and port, with no
   ;; default login credentials.
   (ContentSourceFactory/newContentSource host port))
  ([host port user password]
   (hosted-content-source host port user password nil nil))
  ([host port user password content-base]
   (hosted-content-source host port user password content-base nil))

  ;; Return a ContentSource object that will serve as the source of
  ;; connections to the server on the given host and port, with login
  ;; credentials of the given user and password.
  ([host port user password content-base security-opts]
   ;; TODO enforce or construct SecurityOptions for security-opts
   (ContentSourceFactory/newContentSource host port
                                          user password
                                          content-base
                                          security-opts)))

(defn managed-content-source
  "TODO"
  [cxn-provider user password content-base]
  ;; Return a ContentSource object that will use the provided
  ;; ConnectionProvider instance to obtain server connections, with the
  ;; given default login credentials and contentbase values.
  ;; XXX advanced
  (when-not (instance? ConnectionProvider cxn-provider)
    (throw (IllegalArgumentException. "Content Source cxn-provider must be a ConnectionProvider")))
  (ContentSourceFactory/newContentSource cxn-provider
                                         user password
                                         content-base))




(defn- create-session*
  "Creates session, given map of database info. If complex connection
  options are necessary, pass in a preconfigured content source."
  [{:keys [uri user password content-base]} & [content-source]]
  (let [cs (if (instance? ContentSource content-source)
             content-source
             (ContentSourceFactory/newContentSource (URI. uri)))]
    (cond
      (and (nil? content-base)
           (or (nil? user)
               (nil? password))) (.newSession cs)

      (and (seq content-base)
           (or (nil? user)
               (nil? password))) (.newSession cs content-base)

      (and (seq user)
           (seq password)
           (nil? content-base)) (.newSession cs user password)

      (and (seq user)
           (seq password)
           (seq content-base)) (.newSession cs user password content-base))))

(defn create-session
  ;; TODO document content-source parameter
  "Create a Session for querying and transacting with. Parameter
  `db-info` describing database connection information must
  include :uri key, and may optionally include connection information
  for :content-base (database name), and/or :user and :password.

  See `newSession` methods at
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/ContentSource.html
  for detail on allowed parameter arrangements.

  If optional `options` map is passed, the session is configured
  accordingly. See
  https://docs.marklogic.com/javadoc/xcc/com/marklogic/xcc/Session.html
  for valid options. (Note that request options are distinct from
  session options, though *default* request options can be set for the
  session.)

  If optional `content-source` is passed, the Session is created from
  the given ContentSource rather than creating one from the database info URI."
  ([db-info]
   (create-session* db-info))
  ([db-info {:keys [default-request-options logger user-object
                    transaction-mode transaction-timeout]}]
   (create-session db-info
                   nil
                   {:default-request-options default-request-options
                    :logger logger :user-object user-object
                    :transaction-mode transaction-mode
                    :transaction-timeout transaction-timeout}))
  ([db-info content-source {:keys [default-request-options logger user-object
                                   transaction-mode transaction-timeout]}]
   (let [session (create-session* db-info content-source)]
     (when (map? default-request-options)
       (.setDefaultRequestOptions session (request-options default-request-options)))
     (when (instance? Logger logger)
       (.setLogger session logger))
     ;; XXX the following is not the strictest test!
     (when (instance? Object user-object)
       (.setUserObject session user-object))
     (when (keyword? transaction-mode)
       (.setTransactionMode session (transaction-modes transaction-mode)))
     (when (integer? transaction-timeout)
       (.setTransactionTimeout session transaction-timeout))
     session)))

;; TODO require privilege
;; (defn session-cb-metadata
;;   "TODO"
;;   [session]
;;   (let [cbmd (.getContentbaseMetaData session)]
;;     {:session (.getSession cbmd) ;; XXX unnecessary...right?
;;      :user (.getUser cbmd)
;;      :cb-id (.getContentBaseId cbmd)
;;      :cb-name (.getContentBaseName cbmd)
;;      :driver-major-version (.getDriverMajorVersion cbmd)
;;      :driver-minor-version (.getDriverMinorVersion cbmd)
;;      :driver-patch-version (.getDriverPatchVersion cbmd)
;;      :driver-version (.getDriverVersionString cbmd)
;;      ;; :server-major-version (.getServerMajorVersion cbmd)
;;      ;; :server-minor-version (.getServerMinorVersion cbmd)
;;      ;; :server-patch-version (.getServerPatchVersion cbmd)
;;      ;; :server-version (.getServerVersionString cbmd)
;;      ;; :forest-ids (seq (.getForestIds cbmd))
;;      ;; :forest-maps (.getForestMap cbmd)
;;      ;; :forest-names (seq (.getForestNames cbmd))
;;      }
;;     ))

(defn session-options
  "Returns a map describing all options on the given Session object."
  [session]
  {:default-request-options (.getDefaultRequestOptions session)
   :effective-request-options (.getEffectiveRequestOptions session)
   :logger (.getLogger session)
   ;; :contentbase-metadata (session-cb-metadata session) TODO
   :xaresource (.getXAResource session)
   :user-object (.getUserObject session)
   :transaction-timeout (.getTransactionTimeout session)
   :transaction-mode ((clojure.set/map-invert transaction-modes)
                      (.getTransactionMode session))})
{'fraction_non_alphanumeric': 0.15434436382941072, 'fraction_numerical': 0.0013448947089010795, 'mean_word_length': 3.4129314383882554, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 19, 'https://': 20, 'lorem ipsum': 0, 'www.': 0, 'xml': 6}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 15, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1022578', 'n_tokens_mistral': 8887, 'n_tokens_neox': 8077, 'n_words': 2319}