hara.io tools for files and io operations

Author: Chris Zheng  (z@caudate.me)
Date: 29 June 2017
Repository: https://github.com/zcaudate/hara
Version: 2.5.10

hara.io provide a set of utilities to work with the filesystem and the user display

1    io.ansii

Add to project.clj dependencies:

[im.chit/hara.io.ansii "2.5.10"]

hara.io.ansii provides an easier interface for working with display colors



define-ansii-forms ^

defines ansii forms given by the lookups

v 2.4
(defn define-ansii-forms
  []
  (->> (dissoc lookup :reset)
       (keys)
       (mapv (comp eval ansii-form))))
link
;; Text: ;; [blue cyan green grey magenta red white yellow] (blue "hello") => "hello" ;; Background: ;; [on-blue on-cyan on-green on-grey ;; on-magenta on-red on-white on-yellow] (on-white "hello") => "hello" ;; Attributes: ;; [blink bold concealed dark reverse-color underline] (blink "hello") => "hello"

encode ^

encodes the ansii characters for modifiers

v 2.4
(defn encode
  [modifier]
  (if-let [i (lookup modifier)]
    (str "033[" i "m")
    (throw (Exception. (str "Modifier not available: " modifier)))))
link
(encode :bold) => "" (encode :red) => ""

style ^

styles the text according to the modifiers

v 2.4
(defn style
  [text modifiers]
  (str (string/join (map encode modifiers)) text (encode :reset)))
link
(style "hello" [:bold :red]) => "hello"

2    io.archive

Add to project.clj dependencies:

[im.chit/hara.io.archive "2.5.10"]

hara.io.archive allows an easy interface for working with zip files



archive ^

puts files into an archive

v 2.4
(defn archive
  ([archive root]
   (let [ach (open archive)
         res (archive/-archive ach
                               root
                               (fs/select root {:exclude [fs/directory?]}))]
     (.close ach)
     res))
  ([archive root inputs]
   (archive/-archive (open archive) root inputs)))
link
(archive "hello/stuff.jar" "src")

extract ^

extracts all file from an archive

v 2.4
(defn extract
  ([archive]
   (extract archive (fs/parent (url archive))))
  ([archive output]
   (extract archive output (list archive)))
  ([archive output entries]
   (archive/-extract (open archive) output entries)))
link
(extract "hello/stuff.jar") (extract "hello/stuff.jar" "output") (extract "hello/stuff.jar" "output" ["world.java"])

has? ^

checks if the archive has a particular entry

v 2.4
(defn has?
  [archive entry]
  (archive/-has? (open archive) entry))
link
(has? "hello/stuff.jar" "world.java") => false

insert ^

inserts a file to an entry within the archive

v 2.4
(defn insert
  [archive entry input]
  (archive/-insert (open archive) entry input))
link
(insert "hello/stuff.jar" "world.java" "path/to/world.java")

list ^

lists all the entries in the archive

v 2.4
(defn list
  [archive]
  (archive/-list (open archive)))
link
(list "hello/stuff.jar") ;;=> [#path:"/"]

open ^

either opens an existing archive or creates one if it doesn't exist

v 2.4
(defn open
  [archive]
  (cond (instance? FileSystem archive)
        archive

        :else
        (let [path (fs/path archive)]
          (cond (fs/exists? path)
                (FileSystems/newFileSystem path nil)

                :else
                (do (fs/create-directory (fs/parent path))
                    (FileSystems/newFileSystem
                     (URI. (str "jar:file:" path))
                     {"create" "true"}))))))
link
(open "hello/stuff.jar") ;;=> creates a zip-file

path ^

returns the url of the archive

v 2.4
(defn path
  [archive entry]
  (archive/-path archive entry))
link
(-> (open "hello/stuff.jar") (path "world.java") (str)) => "world.java"

remove ^

removes an entry from the archive

v 2.4
(defn remove
  [archive entry]
  (archive/-remove (open archive) entry))
link
(remove "hello/stuff.jar" "world.java")

stream ^

creates a stream for an entry wthin the archive

v 2.4
(defn stream
  [archive entry]
  (archive/-stream (open archive) entry))
link
(stream "hello/stuff.jar" "world.java")

url ^

returns the url of the archive

v 2.4
(defn url
  [archive]
  (archive/-url archive))
link
(url (open "hello/stuff.jar")) => "/Users/chris/Development/chit/lucidity/hello/stuff.jar"

3    io.classloader

Add to project.clj dependencies:

[im.chit/hara.io.classloader "2.5.10"]

hara.io.classloader provides an easier interface for working with jvm classloaders



delegation ^

returns a list of classloaders in order of top to bottom

v 2.2
(defn delegation
  [cl]
  (->> cl
       (iterate (fn [^ClassLoader cl] (.getParent cl)))
       (take-while identity)
       (reverse)))
link
(-> (Thread/currentThread) (.getContextClassLoader) (delegation)) => list?

eval-in ^

given an environment, evaluates a form

v 2.2
(defn eval-in
  [env form]
  (let [thrd (Thread/currentThread)
        curr (.getContextClassLoader thrd)
        _ (.setContextClassLoader thrd (:classloader env))
        string (pr-str form)
        [obj data] (try
                     (-> (str "(let [res " string "] [res (pr-str res)])")
                         ((-> env :fn :read-string))
                         ((-> env :fn :eval)))
                     (finally
                       (.setContextClassLoader thrd curr)))]
    (try (read-string data)
         (catch Exception e
           obj))))
link
(-> (url-classloader [+clojure-jar+ (-> (io/file "scripts") (.getAbsolutePath) (str "/"))]) (new-env) (eval-in '(do (require 'other-code) (eval '(other-code/add 1 2 3 4 5))))) => 15

new-env ^

creates an new environment for isolated class loading

v 2.2
(defn new-env
  ([] (new-env +rt+))
  ([^ClassLoader cl]
   (let [rt (.loadClass cl "clojure.lang.RT")
         cp (.loadClass cl "clojure.lang.Compiler")
         read-string (reflect/query-class rt ["readString" 1 :#])
         eval-form   (reflect/query-class cp ["eval" 1 :#])
         env {:classloader cl
              :rt rt
              :compiler cp
              :fn {:read-string read-string
                   :eval eval-form}}]
     (do (eval-in env '(require (quote clojure.main)
                                (quote clojure.core)))
         env))))
link
(new-env) => (contains-in {:classloader #(instance? ClassLoader %) :rt clojure.lang.RT :compiler clojure.lang.Compiler :fn {:read-string ifn? :eval ifn?}}) (new-env (url-classloader [+clojure-jar+])) => (contains-in {:classloader ClassLoader :rt Class :compiler Class :fn {:read-string ifn?, :eval ifn?}})

to-url ^

constructs a `java.net.URL` object from a string

v 2.2
(defn to-url
  [path]
  (cond (checks/url? path)
        path

        (string? path)
        (java.net.URL. (str "file:" path))

        :else (throw (Exception. "Not Implemented"))))
link
(str (to-url "/dev/null")) => "file:/dev/null"

url-classloader ^

returns a `java.net.URLClassLoader` from a list of strings

v 2.2
(defn url-classloader
  ([urls]
   (url-classloader urls +base+))
  ([urls parent]
   (URLClassLoader. (->> urls
                         (map to-url)
                         (into-array java.net.URL))
                    parent)))
link
(->> (url-classloader ["/dev/null"]) (.getURLs) (map str)) => ["file:/dev/null"]

4    io.classpath

Add to project.clj dependencies:

[im.chit/hara.io.classpath "2.5.10"]

hara.io.classpath allows resolution and searching of classes



all-jars ^

gets all jars, either on the classloader or coordinate

v 2.4
(defn all-jars
  [& [x :as coords]]
  (cond (nil? x)
        (->> (cls/delegation cls/+rt+)
             (mapcat #(.getURLs %))
             (map #(.getFile %))
             (filter #(.endsWith % ".jar")))

        :else
        (map #(artifact/artifact :path %)
             coords)))
link
(-> (all-jars) count) => 150 (-> (all-jars '[org.eclipse.aether/aether-api "1.1.0"]) count) => 1

artifact ^

converts to various artifact formats

v 2.4
(defn artifact
  ([] (-> artifact/artifact
          (.getMethodTable)
          (keys)
          (set)))
  ([x]
   (artifact/artifact nil x))
  ([type x]
   (artifact/artifact type x)))
link
(artifact) => #{:path :coord :default :string} (artifact '[hello "2.5"]) => (contains {:group "hello", :artifact "hello", :extension "jar", :classifier nil, :version "2.5"}) (artifact :string '[hello "2.5"]) => "hello:hello:jar:2.5"

class-seq ^

creates a sequence of class names

v 2.4
(defn class-seq
  ([] (class-seq nil))
  ([coords]
   (->> (for [jar  (map #(artifact/artifact :path %) coords)
              item (archive/list jar)]
          (str item))
        (filter #(.endsWith % ".class"))
        (map #(.substring % 1 (- (.length %) 6)))
        (map #(.replaceAll % "/" ".")))))
link
(-> (all-jars '[org.eclipse.aether/aether-api "1.1.0"]) (class-seq) (count)) => 128

match-jars ^

matches jars from any representation

v 2.4
(defn match-jars
  ([names] (match-jars names []))
  ([names coords]
   (let [patterns (map (fn [name]
                         (->> [name ".*"]
                              (artifact/artifact :path)
                              (re-pattern)))
                       names)]
     (filter (fn [path]
               (some (fn [pattern]
                       (re-find pattern path))
                     patterns))
             (apply all-jars coords)))))
link
(match-jars '[org.eclipse.aether/aether-api "1.1.0"]) => ("<.m2>/org/eclipse/aether/aether-api/1.1.0/aether-api-1.1.0.jar")

resolve-classloader ^

resolves a class or namespace to a physical location

v 2.4
(defn resolve-classloader
  ([x] (resolve-classloader x loader/+rt+))
  ([x loader]
   (if-let [path (-> (common/resource-entry x)
                     (io/resource loader)
                     (.getPath))]
     (cond (.startsWith path "file:")
           (-> (subs path (count "file:"))
               (clojure.string/split #"!/"))

           (.startsWith path "/")
           [nil path]))))
link
(resolve-classloader String) => ["/jre/lib/rt.jar" "java/lang/String.class"] (resolve-classloader 'hara.test) => [nil "/hara/src/hara/test.clj"]

resolve-entry ^

resolves a class or namespace within a context

v 2.4
(defn resolve-entry
  ([x]
   (resolve-entry x nil))
  ([x context]
   (resolve-entry x context {}))
  ([x context {:keys [tag] :or {tag :path} :as opts}]
   (cond (nil? context)
         (resolve-classloader x)
         
         (string? context)
         (resolve-jar-entry x (artifact/artifact tag context) opts)

         (vector? context)
         (let [i (first context)]
           (cond (or (string? i)
                     (instance? IPersistentVector i))
                 (first (keep (fn [context]
                                (->> (artifact/artifact tag context)
                                     (#(resolve-jar-entry x % opts))))
                              context))
                 
                 Symbol (->> (artifact/artifact tag context)
                             (#(resolve-jar-entry x % opts)))

                 :else
                 (throw (Exception. (str "Not supported: " x " " context)))))

         :else
         (throw (Exception. (str "Not supported: " x " " context))))))
link
(resolve-entry 'hara.test "im.chit:hara.test:2.4.8") => ["<.m2>/im/chit/hara.test/2.4.8/hara.test-2.4.8.jar" "hara/test.clj"] (resolve-entry 'hara.test ["im.chit:hara.test:2.4.8" "im.chit:hara.string:2.4.8"] {:tag :coord}) => '[[im.chit/hara.test "2.4.8"] "hara/test.clj"] (resolve-entry 'hara.test '[[im.chit/hara.test "2.4.8"] [im.chit/hara.string "2.4.8"]]) => ["<.m2>/im/chit/hara.test/2.4.8/hara.test-2.4.8.jar" "hara/test.clj"] (resolve-entry 'hara.test '[im.chit/hara.test "2.4.8"]) => ["<.m2>/im/chit/hara.test/2.4.8/hara.test-2.4.8.jar" "hara/test.clj"] (resolve-entry 'hara.test '[im.chit/hara.string "2.4.8"]) => nil

resolve-jar-entry ^

resolves a class or namespace within a jar

v 2.4
(defn resolve-jar-entry
  ([x artifact]
   (resolve-jar-entry x artifact {}))
  ([x artifact {:keys [tag] :or {tag :path}}]
   (let [path  (artifact/artifact :path artifact)
         entry (common/resource-entry x)]
     (if (archive/has? path entry)
       [(if (= :path tag)
          path
          (artifact/artifact tag path))
        entry]))))
link
(resolve-jar-entry 'hara.test '[im.chit/hara.test "2.4.8"]) => ["<.m2>/im/chit/hara.test/2.4.8/hara.test-2.4.8.jar" "hara/test.clj"] (resolve-jar-entry 'hara.test "im.chit:hara.test:2.4.8" {:tag :coord}) => '[[im.chit/hara.test "2.4.8"] "hara/test.clj"]

resource-entry ^

creates a entry-path based on input

v 2.4
(defn resource-entry
  [x]
  (condp = (type x)
    String x
    Symbol (resource-entry-symbol x)
    Class (-> (.getName 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"

search ^

searches a pattern for class names

v 2.4
(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 %)))))
link
(->> (.getURLs cls/+base+) (map #(-> % str (subs (count "file:")))) (filter #(.endsWith % "jfxrt.jar")) (class-seq) (search [#"^javafx.*[A-Za-z0-9]Builder$"]) (take 5)) => (javafx.animation.AnimationBuilder javafx.animation.FadeTransitionBuilder javafx.animation.FillTransitionBuilder javafx.animation.ParallelTransitionBuilder javafx.animation.PathTransitionBuilder)

5    io.encode

Add to project.clj dependencies:

[im.chit/hara.io.encode "2.5.10"]

hara.io.encode allows encoding of bytes to hex and base64 formats



from-base64 ^

turns a base64 encoded string into a byte array

v 2.4
(defn from-base64
  [input]
  (.decode (Base64/getDecoder)
           input))
link
(-> (from-base64 "aGVsbG8=") (String.)) => "hello"

from-hex ^

turns a hex string into a sequence of bytes

v 2.4
(defn from-hex
  [s] 
  (byte-array (map #(apply from-hex-chars %) (partition 2 s))))
link
(String. (from-hex "68656c6c6f")) => "hello"

to-base64 ^

turns a byte array into a base64 encoded string

v 2.4
(defn to-base64
  [bytes]
  (.encodeToString (Base64/getEncoder)
                   bytes))
link
(-> (.getBytes "hello") (to-base64)) => "aGVsbG8="

to-base64-bytes ^

turns a byte array into a base64 encoding

v 2.4
(defn to-base64-bytes
  [bytes]
  (.encode (Base64/getEncoder)
           bytes))
link
(-> (.getBytes "hello") (to-base64-bytes) (String.)) => "aGVsbG8="

to-hex ^

turns a byte array into hex string

v 2.4
(defn to-hex
  [bytes]  
  (String. (to-hex-chars bytes)))
link
(to-hex (.getBytes "hello")) => "68656c6c6f"

to-hex-chars ^

turns a byte array into a hex char array

v 2.4
(defn to-hex-chars
  [bytes]
  (char-array (mapcat hex-chars bytes)))
link

6    io.environment

Add to project.clj dependencies:

[im.chit/hara.io.environment "2.5.10"]

hara.io.environment provides an easier interface for working with jvm properties and versioning.



clojure-version ^

returns the current clojure version

v 2.2
(defn clojure-version
  []
  *clojure-version*)
link
(env/clojure-version) => (contains {:major anything, :minor anything, :incremental anything :qualifier anything})

init ^

only attempts to load the files when the minimum versions have been met

v 2.2
(defmacro init
  [constraints & statements]
  (if (satisfied constraints)
    (let [trans-fn (fn [[k & rest]]
                     (let [sym (symbol (str "clojure.core/" (name k)))]
                       (cons sym (map (fn [w]
                                              (if (keyword? w)
                                                w
                                                (list 'quote w)))
                                      rest))))]
      (cons 'do (map trans-fn statements)))))
link
(env/init {:java {:major 1 :minor 8} :clojure {:major 1 :minor 6}} (:require [hara.time.data.zone java-time-zoneid] [hara.time.data.instant java-time-instant] [hara.time.data.format java-time-format-datetimeformatter]) (:import java.time.Instant))

java-version ^

returns the current java version

v 2.2
(defn java-version
  []
  (let [[major minor incremental qualifier]
        (->> (path/split (System/getProperty "java.version") #"[._-]")
             (take 4)
             (map #(Long/parseLong %)))]
    {:major major
     :minor minor
     :incremental incremental
     :qualifier qualifier}))
link
(env/java-version) => (contains {:major anything, :minor anything, :incremental anything :qualifier anything})

properties ^

returns jvm properties in a nested map for easy access

v 2.2
(defn properties
  []
  (->> (System/getProperties)
     (reduce (fn [out [k v]]
               (conj out [(path/split (keyword k) #".") v]))
             [])
     (sort)
     (reverse)
     (reduce (fn [out [k v]]
               (if (get-in out k)
                 (assoc-in out (conj k :name) v)
                 (assoc-in out k v)))
             (Properties.))))
link
(->> (env/properties) :os) => (contains {:arch anything :name anything :version anything})

run ^

only runs the following code is the minimum versions have been met

v 2.2
(defmacro run
  [constraints & body]
  (if (satisfied constraints)
    (cons 'do body)))
link
(env/run {:java {:major 1 :minor 8} :clojure {:major 1 :minor 6}} (Instant/ofEpochMilli 0))

satisfied ^

only attempts to load the files when the minimum versions have been met

v 2.2
(defn satisfied
  [constraints]
  (let [satisfy (fn [current constraint]
                  (every? (fn [[label val]]
                            (>= (get current label) val))
                          (seq constraint)))]
    (when (every? (fn [[tag constraint]]
                    (let [current (version tag)]
                      (satisfy current constraint)))
                  (seq constraints))
      true)))
link
(env/satisfied {:java {:major 1 :minor 7} :clojure {:major 1 :minor 6}}) => true

version ^

alternate way of getting clojure and java version

v 2.2
(defn version
  [tag]
  (case tag
    :clojure (clojure-version)
    :java    (java-version)))
link
(env/version :clojure) => (env/clojure-version) (env/version :java) => (env/java-version)

7    io.project

Add to project.clj dependencies:

[im.chit/hara.io.project "2.5.10"]

hara.io.project provides an easier interface for working with clojure project.clj files



all-files ^

returns all the clojure files in a directory

v 2.4
(defn all-files
  ([] (all-files ["."]))
  ([paths] (all-files paths {}))
  ([paths opts]
   (all-files paths opts (project)))
  ([paths opts project]
   (let [filt (-> {:include [#".clj$"]}
                  (merge opts)
                  (update-in [:exclude]
                             conj
                             fs/link?))
         result (->> paths
                     (map #(fs/path (:root project) %))
                     (mapcat #(fs/select % filt))
                     (map str)
                     (map (juxt file-namespace identity))
                     (into {}))]
     (dissoc result nil))))
link
(+ (count (all-files ["test/hara"])) (count (all-files ["test/documentation"]))) => (count (all-files ["test"])) (-> (all-files ["test"]) (get 'hara.io.project-test)) => #(.endsWith ^String % "/test/hara/io/project_test.clj")

file-lookup ^

NONE
(defn file-lookup
  ([] (file-lookup (project)))
  ([project]
   (all-files (concat (:source-paths project)
                      (:test-paths project))
              {}
              project)))
example not found

file-namespace ^

reads the namespace of the given path

v 2.4
(defn file-namespace
  [path]
  (try
    (->> (fs/code path)
         (filter #(-> % first (= 'ns)))
         first
         second)
    (catch Throwable t
      (println path "Cannot be loaded"))))
link
(file-namespace "src/hara/io/project.clj") => 'hara.io.project

project ^

returns `project.clj` as a map

v 2.4
(defn project
  ([] (project "project.clj"))
  ([path]
   (let [path  (fs/path path)
         root  (.getParent path)
         pform (read-string (slurp (str path)))
         [_ full version] (take 3 pform)
         group    (or (namespace full)
                      (str full))
         artifact (name full)
         proj  (->> (drop 3 pform)
                    (concat [:name full
                             :artifact artifact
                             :group group
                             :version version
                             :root (str root)])
                    (apply hash-map))
         proj (-> proj
                  (update-in [:source-paths] (fnil identity ["src"]))
                  (update-in [:test-paths] (fnil identity ["test"])))]
     proj)))
link

project-name ^

returns the name, read from project.clj

v 2.4
(defn project-name
  ([] (project-name "project.clj"))
  ([path]
   (-> (fs/code path)
       first
       second)))
link
(project-name) => 'im.chit/hara