aboutsummaryrefslogtreecommitdiff
path: root/src/clojure/contrib/lib/lib.clj
diff options
context:
space:
mode:
Diffstat (limited to 'src/clojure/contrib/lib/lib.clj')
-rw-r--r--src/clojure/contrib/lib/lib.clj483
1 files changed, 483 insertions, 0 deletions
diff --git a/src/clojure/contrib/lib/lib.clj b/src/clojure/contrib/lib/lib.clj
new file mode 100644
index 00000000..c5324b07
--- /dev/null
+++ b/src/clojure/contrib/lib/lib.clj
@@ -0,0 +1,483 @@
+;; Copyright (c) Stephen C. Gilardi. All rights reserved. The use and
+;; distribution terms for this software are covered by the Common Public
+;; License 1.0 (http://opensource.org/licenses/cpl.php) which can be found
+;; in the file CPL.TXT at the root of this distribution. By using this
+;; software in any fashion, you are agreeing to be bound by the terms of
+;; this license. You must not remove this notice, or any other, from this
+;; software.
+;;
+;; clojure.contrib.lib (Lib)
+;;
+;; Lib provides functions for loading and managing Clojure source code
+;; contained in Java resources.
+;;
+;; A 'lib' is a unit of Clojure source code contained in a Java resource
+;; and named by a symbol. The lib's name is used to identify the lib and
+;; to locate the resource that contains it.
+;;
+;; Lib provides functions to:
+;;
+;; - find a resource given a classpath-relative path
+;; - load code from a resource given an absolute path
+;; - load libs from resources given lib names
+;; - load namespace definitions given namespace specs
+;; - ensure namespace definitions have been loaded while avoiding
+;; duplicate loads, and
+;; - create a namespace and refer 'clojure into it succinctly
+;;
+;; Symbols, Namespaces, Packages
+;;
+;; Symbols in Clojure are two-part identifiers - with an optional
+;; namespace and a name, both strings. Namespaces are used to distinguish
+;; two symbols that have the same name. Vars are named by symbols that
+;; must have a namespace part, and thus can be considered to be in
+;; namespaces. [From Clojure documentation]
+;;
+;; Packages in Java play a role similar to Clojure namespaces - they
+;; partition the global namespace to allow large programs to avoid name
+;; conflicts. Java defines a mapping from package names to directories
+;; within classpath: components of a package name separated by periods
+;; correspond to components of a classpath-relative path separated by
+;; slashes. Lib uses this same mapping to locate libs and namespaces in
+;; classpath.
+;;
+;; Loading Libs
+;;
+;; A lib's name provides the location of the resource that contains it in
+;; classpath. The resource name is the last component of the lib's path
+;; followed by ".clj". For example, a lib named 'a.b.c is contained in the
+;; resource "<classpath>/a/b/c.clj".
+;;
+;; Loading Namespaces
+;;
+;; To load a namespace definition, Lib loads the 'root lib' for the
+;; namespace. The root lib's name is derived from the namespace name by
+;; repeating its last component. For example, the root lib for the
+;; namespace 'x.y.z is the lib 'x.y.z.z contained in the resource
+;; "<classpath>/x/y/z/z.clj". The namespace definition may be entirely
+;; within the root lib or it may span multiple libs in the hierarchy of
+;; directories at and under the namespace's directory. In the latter case
+;; the root lib must include commands to (directly or indirectly) load the
+;; remaining libs.
+;;
+;; Nsspecs
+;;
+;; An nsspec specifies a namespace to load. It is either a namespace name
+;; or a list containing the namespace name and zero or more options
+;; expressed as squential keywords and values. Nsspec examples:
+;; - 'clojure.contrib.sql
+;; - '(clojure.contrib.sql)
+;; - '(clojure.contrib.sql :exclude (get-connection)).
+;; - '(clojure.contrib.sql :as sql)
+;;
+;; Prefix Lists
+;;
+;; It is common for Clojure code to depend on several libs or namespaces
+;; whose names have one or more initial components in common. When
+;; specifying lib or namespace names for Lib to use, prefix lists can be
+;; used to reduce repetition in the call. A prefix list is a list
+;; containing the shared prefix followed by lib or namespace names and/or
+;; prefix lists with the shared prefix elided. For example, the following
+;; are all equivalent:
+;; (load-libs 'a.b.c 'a.b.d 'a.c.e 'a.c.f) ; all names fully qualified
+;; (load-libs '(a.b c d) '(a.c e f)) ; 'a.b and 'a.c extracted
+;; (load-libs '(a (b c d) (c e f))) ; all common prefixes extracted
+;; Symbols containing explicit periods and the equivalent prefix lists may
+;; be mixed freely.
+;;
+;; Lib Functions
+;;
+;; Resources
+;;
+;; Function: find-resource
+;; Searches available class loaders for a resource, returns URL or nil
+;;
+;; Function: load-resource
+;; Loads Clojure source from an absolute path: URI, URL or String
+;;
+;; Libs
+;;
+;; Function: load-libs
+;; Loads libs by name from arbitrary locations within classpath
+;;
+;; Namespaces
+;;
+;; Function: load-namespaces
+;; Loads namespace definitions by loading namespace root libs
+;;
+;; Function: namespaces
+;; Returns a sorted set symbols naming namespaces that have been loaded
+;; with the :require option - used to track and avoid duplicate loads.
+;;
+;; Function: require
+;; Loads namespace definitions that have not yet been loaded
+;;
+;; Function: use
+;; Requires namespaces and refers to them using clojure/refer
+;;
+;; Function: init-ns
+;; Creates and enters a namespace and refers to 'clojure and
+;; 'clojure.contrib.lib (use in namespace root libs)
+;;
+;; Examples
+;;
+;; (load-namespaces :require '(clojure.contrib sql sql.tests))
+;; (require '(clojure.contrib sql sql.tests))
+;;
+;; (load-namespaces :require :use '(clojure.contrib sql ns-utils) :verbose)
+;; (use '(clojure.contrib sql ns-utils) :verbose)
+;;
+;; (use :reload-all :verbose
+;; '(clojure.contrib
+;; (sql :exclude (get-connection)
+;; :rename {execute-commands do-commands})
+;; ns-utils))
+;;
+;; scgilardi (gmail)
+;; Created 7 April 2008
+;;
+;; Thanks to Stuart Sierra for providing many useful ideas, discussions
+;; and code contributions for lib.clj.
+
+(clojure/in-ns 'clojure.contrib.lib)
+(clojure/refer 'clojure)
+
+(alias 'set 'clojure.set)
+
+(import '(clojure.lang RT))
+(import '(java.io BufferedReader InputStreamReader))
+(import '(java.net URI URL))
+
+;; Private
+
+(defmacro init-once
+ "Initializes a var exactly once. The var must already exist."
+ {:private true}
+ [var init]
+ `(let [v# (resolve '~var)]
+ (when-not (.isBound v#)
+ (.bindRoot v# ~init))))
+
+(def
+ #^{:private true :doc
+ "A ref to a sorted set of symbols representing loaded namespaces"}
+ *namespaces*)
+(init-once *namespaces* (ref (sorted-set)))
+
+(def
+ #^{:private true :doc
+ "True while a verbose load is pending"}
+ *verbose*)
+(init-once *verbose* false)
+
+(def
+ #^{:private true :doc
+ "A list of keywords that clojure/refer recognizes as filters"}
+ *filter-keys*)
+(init-once *filter-keys* '(:exclude :only :rename))
+
+(def
+ #^{:private true :doc
+ "A vector of the available class loaders ordered by the degree to which
+ they are controlled by Clojure. The root loader's classpath can be
+ extended with clojure/add-classpath"}
+ *class-loaders*
+ (let [root (.ROOT_CLASSLOADER RT)
+ runtime (.getClassLoader (identity RT))
+ system (.getSystemClassLoader ClassLoader)]
+ (if (= system runtime)
+ [root system]
+ [root runtime system])))
+
+(defn- format
+ "Formats a string using String/format"
+ [fmt & args]
+ (String/format fmt (to-array args)))
+
+(defn- printf
+ "Prints formatted output"
+ [fmt & args]
+ (print (apply format fmt args)))
+
+(defn- throw-if
+ "Throws an exception with a message if pred is true. See
+ java.util.Formatter for format string syntax."
+ [pred fmt & args]
+ (when pred
+ (let [message (apply format fmt args)
+ exception (Exception. message)
+ raw-trace (.getStackTrace exception)
+ boring? #(not= (.getMethodName %) "doInvoke")
+ trace (into-array (drop 2 (drop-while boring? raw-trace)))]
+ (.setStackTrace exception trace)
+ (throw exception))))
+
+(defn- nsspec?
+ "Returns true if x is an nsspec"
+ [x]
+ (or (symbol? x)
+ (nil? (second x))
+ (keyword? (second x))))
+
+(defn- prepend
+ "Prepends a symbol or collection to coll"
+ [x coll]
+ (if (symbol? x)
+ (cons x coll)
+ (concat x coll)))
+
+(def find-resource) ; forward declaration
+(def load-resource) ; forward declaration
+
+(defn- load-one
+ "Loads one lib from a resource at url. If need-ns is 'true' ensures that
+ the namespace named by sym exists after loading. If require is 'true'
+ also records the namespace named by sym as loaded so any duplicate loads
+ can be skipped."
+ [url need-ns sym require]
+ (load-resource url)
+ (throw-if (and need-ns (not (find-ns sym)))
+ "namespace '%s' not found after loading '%s'" sym url)
+ (when require
+ (dosync
+ (commute *namespaces* conj sym))))
+
+(defn- load-all
+ "Loads a lib from a resource at url and forces a load of any namespaces
+ it directly or indirectly loads via require/use/load-namespaces"
+ [url need-ns sym require]
+ (dosync
+ (commute *namespaces* set/union
+ (binding [*namespaces* (ref (sorted-set))]
+ (load-one url need-ns sym require)
+ @*namespaces*))))
+
+(defn- name-path
+ "Returns a classpath-relative path given a symbol name"
+ [name]
+ (.. name
+ (replace \- \_)
+ (replace \. \/)))
+
+(defn- lib-path
+ "Returns the resource path for a lib"
+ [lib-sym]
+ (str (name-path (name lib-sym)) ".clj"))
+
+(defn- root-lib-path
+ "Returns the resource path for a namespace root lib"
+ [ns-sym]
+ (let [n (name-path (name ns-sym))
+ i (inc (.lastIndexOf n (int \/)))
+ leaf (.substring n i)]
+ (str n \/ leaf ".clj")))
+
+(defn- load-lib
+ "Loads a lib with options: sequential keywords and arguments."
+ [prefix sym & options]
+ (let [sym (symbol (str prefix (when prefix \.) sym))
+ opts (apply hash-map options)
+ as (:as opts)
+ reload (:reload opts)
+ reload-all (:reload-all opts)
+ require (:require opts)
+ root (:root opts)
+ use (:use opts)
+ verbose (:verbose opts)
+ loaded (contains? @*namespaces* sym)
+ load (cond reload-all
+ load-all
+ (or reload (not require) (not loaded))
+ load-one)
+ need-ns (or as use)
+ path ((if root root-lib-path lib-path) sym)
+ url (find-resource path)
+ filter-opts (select-keys opts *filter-keys*)]
+ (binding [*verbose* (or *verbose* verbose)]
+ (when load
+ (throw-if (not url) "'%s' not found in classpath" path)
+ (when *verbose*
+ (printf "(clojure.contrib.lib/load-resource \"%s\")\n" url)
+ (flush))
+ (load url need-ns sym require))
+ (throw-if (and need-ns (not (find-ns sym)))
+ "namespace '%s' not found" sym)
+ (when (and need-ns *verbose*)
+ (printf "(clojure/in-ns '%s)\n" (ns-name *ns*)))
+ (when as
+ (when *verbose*
+ (printf "(clojure/alias '%s '%s)\n" as sym))
+ (alias as sym))
+ (when use
+ (when *verbose*
+ (printf "(clojure/refer '%s" sym)
+ (doseq opt filter-opts
+ (printf " %s '%s" (key opt) (print-str (val opt))))
+ (printf ")\n"))
+ (apply refer sym (mapcat seq filter-opts))))))
+
+(defn- load-prefix-list
+ "Loads libs and handles (nested) prefix lists"
+ [load? prefix opts & args]
+ (doseq arg args
+ (if (load? arg)
+ (apply load-lib prefix (prepend arg opts))
+ (let [[nested-prefix & nested-args] arg]
+ (throw-if (nil? nested-prefix) "prefix cannot be nil")
+ (apply load-prefix-list
+ load?
+ (symbol (str prefix (when prefix \.) nested-prefix))
+ opts
+ nested-args)))))
+
+;; Resources
+
+(defn find-resource
+ "Searches for a resource given a classpath-relative path using available
+ ClassLoaders. If the resource is found, returns its URL, otherwise nil."
+ [rel-path]
+ (some #(.findResource % rel-path) *class-loaders*))
+
+(defn load-resource
+ "Loads Clojure source from a resource specified by an absolute path. The
+ path may be a URI, URL, or String. Accepts any URI scheme supported by
+ URLConnection (http and jar), plus file paths."
+ [abs-path]
+ (let [url (cond (instance? URL abs-path)
+ abs-path
+ (instance? URI abs-path)
+ (.toURL abs-path)
+ (string? abs-path)
+ (URL. abs-path))]
+ (throw-if (not url) "Cannot coerce %s to %s" (class abs-path) URL)
+ (with-open reader
+ (BufferedReader.
+ (InputStreamReader.
+ (.openStream url)))
+ (.load Compiler reader (.getPath url) (.getFile url)))))
+
+;; Libs
+
+(defn load-libs
+ "Loads libs - Clojure source contained in resources in classpath. Each
+ argument is a either a symbol that identifies a lib, a prefix list that
+ identifies multiple libs with names that share common prefixes, or a flag
+ that modifies how all the identified libs are loaded. Symbol names map to
+ paths within classpath: components of the symbol name separated by
+ periods correspond to components in classpath-relative paths separated by
+ slashes. The full resource path is the mapped path with '.clj'
+ appended. For example, the resource containing the lib 'a.b.c is
+ '<classpath>/a/b/c.clj'.
+
+
+ Multiple libs whose names share the same period-delimited prefix can be
+ identified succinctly using a prefix list: a list containing the shared
+ prefix followed by symbols and/or prefix lists with the shared prefix
+ elided. For example, the following are all equivalent:
+ (load-libs 'a.b.c 'a.b.d 'a.c.e 'a.c.f) ; all names fully qualified
+ (load-libs '(a.b c d) '(a.c e f)) ; 'a.b and 'a.c extracted
+ (load-libs '(a (b c d) (c e f))) ; all common prefixes extracted
+ Symbols containing explicit periods and the equivalent prefix lists may
+ be mixed freely.
+
+ Recognized flags: :reload-all, :verbose
+
+ :reload-all forces loading of all namespaces that the identified libs
+ directly or indirectly load via load-namespaces/require/use
+ :verbose triggers printing information about each load and refer"
+ [& args]
+ (let [flags (filter keyword? args)
+ opts (interleave flags (repeat true))
+ args (filter (complement keyword?) args)]
+ (apply load-prefix-list symbol? nil opts args)))
+
+;; Namespaces
+
+(defn load-namespaces
+ "Loads namespace definitions contained in resources in classpath.
+ Each argument is a either an nsspec that identifies a namespace, a prefix
+ list that identifies multiple namespaces with names that share common
+ prefixes, or a flag that modifies how all the identified namespaces are
+ loaded. Namespace names map to paths within classpath: components of the
+ namespace name separated by periods correspond to components in
+ classpath-relative paths separated by slashes. To load the namespace
+ definition, load-namespaces loads the namespace's 'root lib'. The root
+ lib's name is derived from the namespace name by repeating its last
+ component. The root lib for namespace 'a.b.c is the lib 'a.b.c.c
+ contained in the resource '<classpath>/a/b/c/c.clj'. The namespace
+ definition need not be completely contained in its root lib. If it's not
+ the root lib must contain code to (directly or indirectly) load the
+ additional libs using load-libs.
+
+ An nsspec is a symbol or a list containing a symbol and options. Some
+ options may have meaning only when specific flags are set.
+
+ An option is a keyword followed by an argument.
+ Recognized options: :as :exclude, :only, :rename
+
+ The argument for :as is a symbol to use as an alias for the specified
+ namespace.
+ The arguments and semantics for :exclude, :only, and :rename are those
+ documented for clojure/refer. They are only effective when the :use flag
+ is present.
+
+ Multiple namespaces whose names share the same period-delimited prefix
+ can be identified succinctly using a prefix list: a list containing the
+ shared prefix followed by nsspecs and/or prefix lists with the shared
+ prefix elided. For example, the following are all equivalent:
+ (load-namespaces 'a.b.c 'a.b.d 'a.c.e 'a.c.f) ; fully qualified names
+ (load-namespaces '(a.b c d) '(a.c e f)) ; 'a.b and 'a.c extracted
+ (load-namespaces '(a (b c d) (c e f))) ; all common prefixes extracted
+ Symbols containing explicit periods and the equivalent prefix lists may be
+ mixed freely.
+
+ A flag is a keyword.
+ Recognized flags: :require, :use, :reload, :reload-all, :verbose
+
+ :require indicates that any identified namespace definitions that are
+ already loaded need not be reloaded
+ :use triggers referring to each namespace with optional filters specified
+ in its options
+ :reload forces loading of all the identified namespace definitions even
+ if they are already loaded. :reload supersedes :require
+ :reload-all implies :reload and also forces loading of all namespace
+ definitions that the identified namespace definitions directly or
+ indirectly load via load-namespaces/require/use
+ :verbose triggers printing information about each load, alias and refer"
+ [& args]
+ (let [flags (cons :root (filter keyword? args))
+ opts (interleave flags (repeat true))
+ args (filter (complement keyword?) args)]
+ (apply load-prefix-list nsspec? nil opts args)))
+
+(defn namespaces
+ "Returns a sorted set of symbols naming loaded namespaces"
+ []
+ @*namespaces*)
+
+(defn require
+ "Ensures that namespaces are loaded while avoiding duplicate
+ loads. Arguments are described in the docs for 'load-namespaces. The
+ :require flag is automatically set."
+ [& args]
+ (apply load-namespaces :require args))
+
+(defn use
+ "Ensures that namespaces are loaded while avoiding duplicate loads and
+ refers to them in the current namespace. Arguments are described in the
+ docs for 'load-namespaces. The :require and :use flags are automatically
+ set."
+ [& args]
+ (apply load-namespaces :require :use args))
+
+(defn init-ns
+ "Creates ns (if necessary), makes it current, and uses clojure/refer to
+ make the 'clojure namespace available to it. Optional clojure-filters can
+ be passed along to clojure/refer to modify how much of and how 'clojure
+ is pulled in. init-ns also refers to (all of) 'clojure.contrib.lib.
+ Namespace root libs will typically begin with a call to init-ns; any
+ subordinate libs can begin with a call to in-ns."
+ [ns & clojure-filters]
+ (in-ns ns)
+ (apply refer 'clojure clojure-filters)
+ (refer 'clojure.contrib.lib))