jvm.artifact

Maven coordinates, repository paths, and artifact representations

Normalize dependency coordinates and convert them between records, vectors, strings, paths, files, and repository metadata.

1    Overview

jvm.artifact centers on the Rep record. A representation stores group, artifact, extension, classifier, version, scope, exclusions, properties, and file information in one value.

2    Walkthrough

2.1    Normalize coordinates

(require '[jvm.artifact :as artifact])

(def clojure-rep
  (artifact/rep '[org.clojure/clojure "1.11.1"]))

(artifact/rep? clojure-rep)
(artifact/rep->coord clojure-rep)
(artifact/rep->string clojure-rep)
(artifact/rep->path clojure-rep)

2.2    Convert output formats

Use artifact when the caller chooses an output tag dynamically. Direct conversion functions are clearer when the format is fixed.

(artifact/artifact :string '[org.clojure/clojure "1.11.1"])
(artifact/artifact :path "org.clojure:clojure:1.11.1")
(artifact/artifact :default '[org.clojure/clojure "1.11.1"])

3    Supporting namespaces

jvm.artifact.common contains repository path and resource-entry helpers. jvm.artifact.search searches artifact contents and class entries.

4    API



artifact ^

[x] [type x]
Added 3.0

converts various artifact formats

v 3.0
(defn artifact
  ([x]
   (artifact :default x))
  ([type x]
   (protocol.classloader/-artifact type x)))
link
(artifact :string '[hara/hara "2.4.0"]) => "hara:hara:jar:2.4.0" (artifact :path "hara:hara:2.4.0") => (str base/*local-repo* "/hara/hara/2.4.0/hara-2.4.0.jar")

artifact-coord ^

[x] [_ x]
Added 3.0

converts an artifact in any format to the coord representation

v 3.0
(invoke/definvoke artifact-coord
  [:method {:multi protocol.classloader/-artifact
            :val   :coord}]
  ([x]
   (artifact-coord nil x))
  ([_ x]
   (-> (rep x) rep->coord)))
link
(artifact-coord "hara:hara:jar:2.4.0") => '[hara/hara "2.4.0"]

artifact-default ^

[x] [_ x]
Added 3.0

converts an artifact in any format to the default representation

v 3.0
(invoke/definvoke artifact-default
  [:method {:multi protocol.classloader/-artifact
            :val   :default}]
  ([x]
   (artifact-default nil x))
  ([_ x]
   (rep x)))
link
(artifact-default '[hara/hara "2.4.0"]) => rep?

artifact-path ^

[x] [_ x]
Added 3.0

converts an artifact in any format to the path representation

v 3.0
(invoke/definvoke artifact-path
  [:method {:multi protocol.classloader/-artifact
            :val   :path}]
  ([x]
   (artifact-path nil x))
  ([_ x]
   (if (and (string? x)
            (.endsWith ^String x "jar"))
     x
     (-> (rep x)
         rep->path))))
link
(artifact-path '[hara/hara "2.4.0"]) => (str base/*local-repo* "/hara/hara/2.4.0/hara-2.4.0.jar")

artifact-string ^

[x] [_ x]
Added 3.0

converts an artifact in any format to the string representation

v 3.0
(invoke/definvoke artifact-string
  [:method {:multi protocol.classloader/-artifact
            :val   :string}]
  ([x]
   (artifact-string nil x))
  ([_ x]
   (-> (rep x) rep->string)))
link
(artifact-string '[hara/hara "2.4.0"]) => "hara:hara:jar:2.4.0"

artifact-symbol ^

[x] [_ x]
Added 3.0

converts an artifact in any format to the symbol representation

v 3.0
(invoke/definvoke artifact-symbol
  [:method {:multi protocol.classloader/-artifact
            :val   :symbol}]
  ([x]
   (artifact-symbol nil x))
  ([_ x]
   (-> (rep x) rep->coord first)))
link
(artifact-symbol '[hara/hara "2.4.0"]) => 'hara/hara

coord->rep ^

[[name version & {:keys [scope exclusions]}]]
Added 3.0

converts a coord to a rep instance

v 3.0
(defn coord->rep
  ([[name version & {:keys [scope exclusions]}]]
   (let [[group artifact] (clojure.string/split (str name) #"/")
         artifact (or artifact
                      group)]
     (Rep. group artifact "jar" nil version {} nil scope exclusions))))
link
(coord->rep '[hara/hara "2.4.0"]) => (contains {:group "hara" :artifact "hara" :version "2.4.0"})

coord? ^

[obj]
Added 3.0

checks for a valid coordinate

v 3.0
(defn coord?
  ([obj]
   (and (vector? obj)
        (symbol? (first obj))
        (string? (second obj)))))
link
(coord? '[org.clojure/clojure "1.8.0"]) => true (coord? '[1 2 3]) => false

path->rep ^

[x]
Added 3.0

converts a path to a rep instance

v 3.0
(defn path->rep
  ([x]
   (let [arr (->> (re-pattern base/*sep*)
                  (clojure.string/split (.replaceAll ^String x base/*local-repo* ""))
                  (remove empty?))
         extension (-> (last arr)
                       (clojure.string/split #".")
                       last)
         version   (last (butlast arr))
         artifact  (last (butlast (butlast arr)))
         group     (clojure.string/join "." (butlast (butlast (butlast arr))))]
     (Rep. group artifact extension nil version {} x nil nil nil nil))))
link
(path->rep (str base/*local-repo* "/hara/hara/2.4.0/hara-2.4.0.jar")) => (contains {:group "hara" :artifact "hara" :version "2.4.0"})

rep ^

[obj]
Added 3.0

converts various formats to a rep

v 3.0
(defn rep
  ([obj]
   (protocol.classloader/-rep obj)))
link
(str (rep '[hara/hara "2.4.0"])) => "hara:hara:jar:2.4.0" (str (rep "hara:hara:2.4.0")) => "hara:hara:jar:2.4.0"

rep->coord ^

[{:keys [group artifact version exclusions scope]}]
Added 3.0

encodes the rep to a coordinate

v 3.0
(defn rep->coord
  ([{:keys [group artifact version exclusions scope]}]
   (filterv identity (concat [(symbol group artifact) version]
                             (if exclusions [:exclusions exclusions])
                             (if scope [:scope scope])))))
link
(-> {:group "hara" :artifact "hara" :version "2.4.0"} (map->Rep) (rep->coord)) => '[hara/hara "2.4.0"]

rep->path ^

[{:keys [group artifact version extension]}]
Added 3.0

encodes the rep to a path

v 3.0
(defn rep->path
  ([{:keys [group artifact version extension]}]
   (clojure.string/join base/*sep*
             [base/*local-repo* (.replaceAll ^String group "\." base/*sep*)
              artifact version (str artifact "-" version "." (or extension "jar"))])))
link
(-> {:group "hara" :artifact "hara" :version "2.4.0"} (map->Rep) (rep->path)) => #"/hara/hara/2.4.0/hara-2.4.0.jar"

rep->string ^

[{:keys [group artifact extension version]}]
Added 3.0

encodes the rep to a string

v 3.0
(defn rep->string
  ([{:keys [group artifact extension version]}]
   (clojure.string/join ":" [group
                  artifact
                  (if extension
                    (str extension ":" version)
                    version)])))
link
(-> {:group "hara" :artifact "hara" :version "2.4.0"} (map->Rep) (rep->string)) => "hara:hara:2.4.0"

rep-default ^

[x]
Added 3.0

creates the default representation of a artifact

v 3.0
(invoke/definvoke rep-default
  [:method {:multi protocol.classloader/-rep
            :val :default}]
  ([x]
   (cond (instance? Rep x) x

         (map? x) (map->Rep x)

         (coord? x) (coord->rep x)

         (string? x)
         (if (.startsWith ^String x base/*local-repo*)
           (path->rep x)
           (string->rep x))

         (symbol? x)
         (coord->rep [x])

         :else
         (throw (Exception. (str "Invalid form: (" (type x)  ") " x))))))
link
(into {} (rep-default "hara:hara:2.4.0")) => {:properties {}, :group "hara", :classifier nil, :file nil, :exclusions nil, :scope nil, :extension "jar", :artifact "hara", :version "2.4.0"}

rep? ^

[obj]
Added 3.0

checks if an object is of type `jvm.artifact.Rep`

v 3.0
(defn rep?
  ([obj]
   (instance? Rep obj)))
link
(rep? (rep "hara:hara:2.4.0")) => true

string->rep ^

[s]
Added 3.0

converts a string to a rep instance

v 3.0
(defn string->rep
  ([s]
   (let [[group artifact extension? classifer? version :as array] (clojure.string/split s #":")]
     (case (count array)
       1 (path->rep s)
       2 (Rep. group artifact "jar" nil nil {} nil nil nil)
       3 (Rep. group artifact "jar" nil extension? {} nil nil nil)
       4 (Rep. group artifact extension? nil classifer? {} nil nil nil)
       5 (Rep. group artifact extension? classifer? version {} nil nil nil)))))
link
(string->rep "hara:hara:2.4.0") => (contains {:group "hara" :artifact "hara" :version "2.4.0"})


*java-class-path* ^

NONE
(defonce ^:dynamic *java-class-path*
  (->> (clojure.string/split (System/getProperty "java.class.path") #":")
       (filter (fn [^String x] (.endsWith x ".jar")))))
link

*java-home* ^

NONE
(defonce ^:dynamic *java-home*
  (System/getProperty "java.home"))
link

*java-runtime-jar* ^

NONE
(defonce ^:dynamic *java-runtime-jar*
  (str *java-home* "/lib/rt.jar"))
link

*local-repo* ^

NONE
(defonce ^:dynamic *local-repo*
  (-> (System/getProperty "user.home")
      (str *sep* ".m2" *sep* "repository")))
link

*sep* ^

NONE
(defonce ^:dynamic *sep*
  (System/getProperty "file.separator"))
link

resource-entry ^

[x]
Added 3.0

creates a entry-path based on input

v 3.0
(defn resource-entry
  ([x]
   (condp = (type x)
     String x
     Symbol (resource-entry-symbol x)
     Class (-> (.getName ^Class x)
               (.replaceAll "\." *sep*)
               (str  ".class")))))
link
(resource-entry "hello/world.txt") => "hello/world.txt" (resource-entry 'version-clj.core) => "version_clj/core.clj" (resource-entry java.io.File) => "java/io/File.class"

resource-entry-symbol ^

[sym]
Added 3.0

creates a path based on a symbol

v 3.0
(defn resource-entry-symbol
  ([sym]
   (let [sym-str (-> (str sym)
                     (.replaceAll "\." *sep*)
                     (.replaceAll "-" "_"))
         f-char (-> sym-str (clojure.string/split (re-pattern *sep*)) last first)]

     (str sym-str
          (if (<= (int A) (int f-char) (int Z))
            ".class"
            ".clj")))))
link
(resource-entry-symbol 'code.test) => "code/test.clj" (resource-entry-symbol 'clojure.lang.AFn) => "clojure/lang/AFn.class"


class-seq ^

[] [coords]
Added 3.0

creates a sequence of class names

v 3.0
(defn class-seq
  ([] (class-seq nil))
  ([coords]
   (->> (for [jar  (map #(artifact/artifact :path %) coords)
              item (archive/list jar)]
          (str item))
        (filter #(.endsWith ^String % ".class"))
        (map #(.substring ^String % 1 (- (.length ^String %) 6)))
        (map #(.replaceAll ^String % "/" ".")))))
link
(some #{"clojure.lang.RT"} (class-seq '[[org.clojure/clojure "1.12.0"]])) => "clojure.lang.RT"

search ^

[matches classes]
Added 3.0

searches a pattern for class names

v 3.0
(defn search
  ([matches classes]
   (let [match-fns (map search-match matches)
         pre-fns  (filter #(-> % meta :pre) match-fns)
         post-fns (filter #(-> % meta :post) match-fns)]
     (->> classes
          (filter (fn [cls] (every? #(% cls) pre-fns)))
          (map #(Class/forName %))
          (filter (fn [cls] (every? #(% cls) post-fns)))
          (sort-by #(.getName ^Class %))))))
link
(->> (search [#"^java.lang." java.lang.CharSequence] ["java.lang.String" "java.util.ArrayList"]) (map #(.getName ^Class %))) => ["java.lang.String"]

search-match ^

[match]
Added 3.0

constructs a matching function for filtering

v 3.0
(defn search-match
  ([match]
   (cond (fn? match)
         ^:post (fn [cls]
                  (boolean (match cls)))

         (instance? Class match)
         ^:post (fn [cls] (.isAssignableFrom ^Class match cls))

         (instance? java.util.regex.Pattern match)
         ^:pre (fn [cls] (boolean (re-find match cls)))

         (string? match)
         (let [pat (re-pattern match)]
           ^:pre (fn [cls]
                   (boolean (re-find pat cls))))

         :else (throw (Exception. (str "Cannon compile match: " match))))))
link
((search-match #"hello") "hello.world") => true ((search-match java.util.List) java.util.ArrayList) => true