aboutsummaryrefslogtreecommitdiff
path: root/modules/io/src/main/clojure
diff options
context:
space:
mode:
Diffstat (limited to 'modules/io/src/main/clojure')
-rw-r--r--modules/io/src/main/clojure/clojure/contrib/io.clj564
1 files changed, 564 insertions, 0 deletions
diff --git a/modules/io/src/main/clojure/clojure/contrib/io.clj b/modules/io/src/main/clojure/clojure/contrib/io.clj
new file mode 100644
index 00000000..4d793180
--- /dev/null
+++ b/modules/io/src/main/clojure/clojure/contrib/io.clj
@@ -0,0 +1,564 @@
+;;; io.clj -- duck-typed I/O streams for Clojure
+
+;; by Stuart Sierra, http://stuartsierra.com/
+;; May 13, 2009
+
+;; Copyright (c) Stuart Sierra, 2009. All rights reserved. The use
+;; and distribution terms for this software are covered by the Eclipse
+;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
+;; which can be found in the file epl-v10.html 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.
+
+
+;; This file defines "duck-typed" I/O utility functions for Clojure.
+;; The 'reader' and 'writer' functions will open and return an
+;; instance of java.io.BufferedReader and java.io.BufferedWriter,
+;; respectively, for a variety of argument types -- filenames as
+;; strings, URLs, java.io.File's, etc. 'reader' even works on http
+;; URLs.
+;;
+;; Note: this is not really "duck typing" as implemented in languages
+;; like Ruby. A better name would have been "do-what-I-mean-streams"
+;; or "just-give-me-a-stream", but ducks are funnier.
+
+
+;; CHANGE LOG
+;;
+;; July 23, 2010: Most functions here are deprecated. Use
+;; clojure.java.io
+;;
+;; May 13, 2009: added functions to open writers for appending
+;;
+;; May 3, 2009: renamed file to file-str, for compatibility with
+;; clojure.contrib.java. reader/writer no longer use this
+;; function.
+;;
+;; February 16, 2009: (lazy branch) fixed read-lines to work with lazy
+;; Clojure.
+;;
+;; January 10, 2009: added *default-encoding*, so streams are always
+;; opened as UTF-8.
+;;
+;; December 19, 2008: rewrote reader and writer as multimethods; added
+;; slurp*, file, and read-lines
+;;
+;; April 8, 2008: first version
+
+
+
+(ns
+ ^{:author "Stuart Sierra",
+ :doc "This file defines polymorphic I/O utility functions for Clojure.
+
+ The Streams protocol defines reader, writer, input-stream and
+ output-stream methods that return BufferedReader, BufferedWriter,
+ BufferedInputStream and BufferedOutputStream instances (respectively),
+ with default implementations extended to a variety of argument
+ types: URLs or filenames as strings, java.io.File's, Sockets, etc."}
+ clojure.contrib.io
+ (:refer-clojure :exclude (spit))
+ (:import
+ (java.io Reader InputStream InputStreamReader PushbackReader
+ BufferedReader File OutputStream
+ OutputStreamWriter BufferedWriter Writer
+ FileInputStream FileOutputStream ByteArrayOutputStream
+ StringReader ByteArrayInputStream
+ BufferedInputStream BufferedOutputStream
+ CharArrayReader)
+ (java.net URI URL MalformedURLException Socket)))
+
+
+(def
+ ^{:doc "Name of the default encoding to use when reading & writing.
+ Default is UTF-8."
+ :tag "java.lang.String"}
+ *default-encoding* "UTF-8")
+
+(def
+ ^{:doc "Size, in bytes or characters, of the buffer used when
+ copying streams."}
+ *buffer-size* 1024)
+
+(def
+ ^{:doc "Type object for a Java primitive byte array."}
+ *byte-array-type* (class (make-array Byte/TYPE 0)))
+
+(def
+ ^{:doc "Type object for a Java primitive char array."}
+ *char-array-type* (class (make-array Character/TYPE 0)))
+
+
+(defn ^File file-str
+ "Concatenates args as strings and returns a java.io.File. Replaces
+ all / and \\ with File/separatorChar. Replaces ~ at the start of
+ the path with the user.home system property."
+ [& args]
+ (let [^String s (apply str args)
+ s (.replace s \\ File/separatorChar)
+ s (.replace s \/ File/separatorChar)
+ s (if (.startsWith s "~")
+ (str (System/getProperty "user.home")
+ File/separator (subs s 1))
+ s)]
+ (File. s)))
+
+(def
+ ^{:doc "If true, writer, output-stream and spit will open files in append mode.
+ Defaults to false. Instead of binding this var directly, use append-writer,
+ append-output-stream or append-spit."
+ :tag "java.lang.Boolean"}
+ *append* false)
+
+(defn- assert-not-appending []
+ (when *append*
+ (throw (Exception. "Cannot change an open stream to append mode."))))
+
+;; @todo -- Both simple and elaborate methods for controlling buffering of
+;; in the Streams protocol were implemented, considered, and postponed
+;; see http://groups.google.com/group/clojure-dev/browse_frm/thread/3e39e9b3982f542b
+(defprotocol Streams
+ (reader [x]
+ "Attempts to coerce its argument into an open java.io.Reader.
+ The default implementations of this protocol always return a
+ java.io.BufferedReader.
+
+ Default implementations are provided for Reader, BufferedReader,
+ InputStream, File, URI, URL, Socket, byte arrays, character arrays,
+ and String.
+
+ If argument is a String, it tries to resolve it first as a URI, then
+ as a local file name. URIs with a 'file' protocol are converted to
+ local file names. If this fails, a final attempt is made to resolve
+ the string as a resource on the CLASSPATH.
+
+ Uses *default-encoding* as the text encoding.
+
+ Should be used inside with-open to ensure the Reader is properly
+ closed.")
+ (writer [x]
+ "Attempts to coerce its argument into an open java.io.Writer.
+ The default implementations of this protocol always return a
+ java.io.BufferedWriter.
+
+ Default implementations are provided for Writer, BufferedWriter,
+ OutputStream, File, URI, URL, Socket, and String.
+
+ If the argument is a String, it tries to resolve it first as a URI, then
+ as a local file name. URIs with a 'file' protocol are converted to
+ local file names.
+
+ Should be used inside with-open to ensure the Writer is properly
+ closed.")
+ (input-stream [x]
+ "Attempts to coerce its argument into an open java.io.InputStream.
+ The default implementations of this protocol always return a
+ java.io.BufferedInputStream.
+
+ Default implementations are defined for OutputStream, File, URI, URL,
+ Socket, byte array, and String arguments.
+
+ If the argument is a String, it tries to resolve it first as a URI, then
+ as a local file name. URIs with a 'file' protocol are converted to
+ local file names.
+
+ Should be used inside with-open to ensure the InputStream is properly
+ closed.")
+ (output-stream [x]
+ "Attempts to coerce its argument into an open java.io.OutputStream.
+ The default implementations of this protocol always return a
+ java.io.BufferedOutputStream.
+
+ Default implementations are defined for OutputStream, File, URI, URL,
+ Socket, and String arguments.
+
+ If the argument is a String, it tries to resolve it first as a URI, then
+ as a local file name. URIs with a 'file' protocol are converted to
+ local file names.
+
+ Should be used inside with-open to ensure the OutputStream is
+ properly closed."))
+
+(def default-streams-impl
+ {:reader #(reader (input-stream %))
+ :writer #(writer (output-stream %))
+ :input-stream #(throw (Exception. (str "Cannot open <" (pr-str %) "> as an InputStream.")))
+ :output-stream #(throw (Exception. (str "Cannot open <" (pr-str %) "> as an OutputStream.")))})
+
+(extend File
+ Streams
+ (assoc default-streams-impl
+ :input-stream #(input-stream (FileInputStream. ^File %))
+ :output-stream #(let [stream (FileOutputStream. ^File % *append*)]
+ (binding [*append* false]
+ (output-stream stream)))))
+(extend URL
+ Streams
+ (assoc default-streams-impl
+ :input-stream (fn [^URL x]
+ (input-stream (if (= "file" (.getProtocol x))
+ (FileInputStream. (.getPath x))
+ (.openStream x))))
+ :output-stream (fn [^URL x]
+ (if (= "file" (.getProtocol x))
+ (output-stream (File. (.getPath x)))
+ (throw (Exception. (str "Can not write to non-file URL <" x ">")))))))
+(extend URI
+ Streams
+ (assoc default-streams-impl
+ :input-stream #(input-stream (.toURL ^URI %))
+ :output-stream #(output-stream (.toURL ^URI %))))
+(extend String
+ Streams
+ (assoc default-streams-impl
+ :input-stream #(try
+ (input-stream (URL. %))
+ (catch MalformedURLException e
+ (input-stream (File. ^String %))))
+ :output-stream #(try
+ (output-stream (URL. %))
+ (catch MalformedURLException err
+ (output-stream (File. ^String %))))))
+(extend Socket
+ Streams
+ (assoc default-streams-impl
+ :input-stream #(.getInputStream ^Socket %)
+ :output-stream #(output-stream (.getOutputStream ^Socket %))))
+(extend *byte-array-type*
+ Streams
+ (assoc default-streams-impl :input-stream #(input-stream (ByteArrayInputStream. %))))
+(extend *char-array-type*
+ Streams
+ (assoc default-streams-impl :reader #(reader (CharArrayReader. %))))
+(extend Object
+ Streams
+ default-streams-impl)
+
+(extend Reader
+ Streams
+ (assoc default-streams-impl :reader #(BufferedReader. %)))
+(extend BufferedReader
+ Streams
+ (assoc default-streams-impl :reader identity))
+(defn- inputstream->reader
+ [^InputStream is]
+ (reader (InputStreamReader. is *default-encoding*)))
+(extend InputStream
+ Streams
+ (assoc default-streams-impl :input-stream #(BufferedInputStream. %)
+ :reader inputstream->reader))
+(extend BufferedInputStream
+ Streams
+ (assoc default-streams-impl
+ :input-stream identity
+ :reader inputstream->reader))
+
+(extend Writer
+ Streams
+ (assoc default-streams-impl :writer #(do (assert-not-appending)
+ (BufferedWriter. %))))
+(extend BufferedWriter
+ Streams
+ (assoc default-streams-impl :writer #(do (assert-not-appending) %)))
+(defn- outputstream->writer
+ [^OutputStream os]
+ (assert-not-appending)
+ (writer (OutputStreamWriter. os *default-encoding*)))
+(extend OutputStream
+ Streams
+ (assoc default-streams-impl
+ :output-stream #(do (assert-not-appending)
+ (BufferedOutputStream. %))
+ :writer outputstream->writer))
+(extend BufferedOutputStream
+ Streams
+ (assoc default-streams-impl
+ :output-stream #(do (assert-not-appending) %)
+ :writer outputstream->writer))
+
+(defn append-output-stream
+ "Like output-stream but opens file for appending. Does not work on streams
+ that are already open."
+ {:deprecated "1.2"}
+ [x]
+ (binding [*append* true]
+ (output-stream x)))
+
+(defn append-writer
+ "Like writer but opens file for appending. Does not work on streams
+ that are already open."
+ {:deprecated "1.2"}
+ [x]
+ (binding [*append* true]
+ (writer x)))
+
+(defn write-lines
+ "Writes lines (a seq) to f, separated by newlines. f is opened with
+ writer, and automatically closed at the end of the sequence."
+ [f lines]
+ (with-open [^BufferedWriter writer (writer f)]
+ (loop [lines lines]
+ (when-let [line (first lines)]
+ (.write writer (str line))
+ (.newLine writer)
+ (recur (rest lines))))))
+
+(defn read-lines
+ "Like clojure.core/line-seq but opens f with reader. Automatically
+ closes the reader AFTER YOU CONSUME THE ENTIRE SEQUENCE."
+ [f]
+ (let [read-line (fn this [^BufferedReader rdr]
+ (lazy-seq
+ (if-let [line (.readLine rdr)]
+ (cons line (this rdr))
+ (.close rdr))))]
+ (read-line (reader f))))
+
+(defn ^String slurp*
+ "Like clojure.core/slurp but opens f with reader."
+ {:deprecated "1.2"}
+ [f]
+ (with-open [^BufferedReader r (reader f)]
+ (let [sb (StringBuilder.)]
+ (loop [c (.read r)]
+ (if (neg? c)
+ (str sb)
+ (do (.append sb (char c))
+ (recur (.read r))))))))
+
+(defn spit
+ "Opposite of slurp. Opens f with writer, writes content, then
+ closes f."
+ {:deprecated "1.2"}
+ [f content]
+ (with-open [^Writer w (writer f)]
+ (.write w content)))
+
+(defn append-spit
+ "Like spit but appends to file."
+ {:deprecated "1.2"}
+ [f content]
+ (with-open [^Writer w (append-writer f)]
+ (.write w content)))
+
+(defn pwd
+ "Returns current working directory as a String. (Like UNIX 'pwd'.)
+ Note: In Java, you cannot change the current working directory."
+ {:deprecated "1.2"}
+ []
+ (System/getProperty "user.dir"))
+
+(defmacro with-out-writer
+ "Opens a writer on f, binds it to *out*, and evalutes body.
+ Anything printed within body will be written to f."
+ [f & body]
+ `(with-open [stream# (writer ~f)]
+ (binding [*out* stream#]
+ ~@body)))
+
+(defmacro with-out-append-writer
+ "Like with-out-writer but appends to file."
+ {:deprecated "1.2"}
+ [f & body]
+ `(with-open [stream# (append-writer ~f)]
+ (binding [*out* stream#]
+ ~@body)))
+
+(defmacro with-in-reader
+ "Opens a PushbackReader on f, binds it to *in*, and evaluates body."
+ [f & body]
+ `(with-open [stream# (PushbackReader. (reader ~f))]
+ (binding [*in* stream#]
+ ~@body)))
+
+(defmulti
+ ^{:deprecated "1.2"
+ :doc "Copies input to output. Returns nil.
+ Input may be an InputStream, Reader, File, byte[], or String.
+ Output may be an OutputStream, Writer, or File.
+
+ Does not close any streams except those it opens itself
+ (on a File).
+
+ Writing a File fails if the parent directory does not exist."
+ :arglists '([input output])}
+ copy
+ (fn [input output] [(type input) (type output)]))
+
+(defmethod copy [InputStream OutputStream] [^InputStream input ^OutputStream output]
+ (let [buffer (make-array Byte/TYPE *buffer-size*)]
+ (loop []
+ (let [size (.read input buffer)]
+ (when (pos? size)
+ (do (.write output buffer 0 size)
+ (recur)))))))
+
+(defmethod copy [InputStream Writer] [^InputStream input ^Writer output]
+ (let [^"[B" buffer (make-array Byte/TYPE *buffer-size*)]
+ (loop []
+ (let [size (.read input buffer)]
+ (when (pos? size)
+ (let [chars (.toCharArray (String. buffer 0 size *default-encoding*))]
+ (do (.write output chars)
+ (recur))))))))
+
+(defmethod copy [InputStream File] [^InputStream input ^File output]
+ (with-open [out (FileOutputStream. output)]
+ (copy input out)))
+
+(defmethod copy [Reader OutputStream] [^Reader input ^OutputStream output]
+ (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)]
+ (loop []
+ (let [size (.read input buffer)]
+ (when (pos? size)
+ (let [bytes (.getBytes (String. buffer 0 size) *default-encoding*)]
+ (do (.write output bytes)
+ (recur))))))))
+
+(defmethod copy [Reader Writer] [^Reader input ^Writer output]
+ (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)]
+ (loop []
+ (let [size (.read input buffer)]
+ (when (pos? size)
+ (do (.write output buffer 0 size)
+ (recur)))))))
+
+(defmethod copy [Reader File] [^Reader input ^File output]
+ (with-open [out (FileOutputStream. output)]
+ (copy input out)))
+
+(defmethod copy [File OutputStream] [^File input ^OutputStream output]
+ (with-open [in (FileInputStream. input)]
+ (copy in output)))
+
+(defmethod copy [File Writer] [^File input ^Writer output]
+ (with-open [in (FileInputStream. input)]
+ (copy in output)))
+
+(defmethod copy [File File] [^File input ^File output]
+ (with-open [in (FileInputStream. input)
+ out (FileOutputStream. output)]
+ (copy in out)))
+
+(defmethod copy [String OutputStream] [^String input ^OutputStream output]
+ (copy (StringReader. input) output))
+
+(defmethod copy [String Writer] [^String input ^Writer output]
+ (copy (StringReader. input) output))
+
+(defmethod copy [String File] [^String input ^File output]
+ (copy (StringReader. input) output))
+
+(defmethod copy [*char-array-type* OutputStream] [input ^OutputStream output]
+ (copy (CharArrayReader. input) output))
+
+(defmethod copy [*char-array-type* Writer] [input ^Writer output]
+ (copy (CharArrayReader. input) output))
+
+(defmethod copy [*char-array-type* File] [input ^File output]
+ (copy (CharArrayReader. input) output))
+
+(defmethod copy [*byte-array-type* OutputStream] [^"[B" input ^OutputStream output]
+ (copy (ByteArrayInputStream. input) output))
+
+(defmethod copy [*byte-array-type* Writer] [^"[B" input ^Writer output]
+ (copy (ByteArrayInputStream. input) output))
+
+(defmethod copy [*byte-array-type* File] [^"[B" input ^Writer output]
+ (copy (ByteArrayInputStream. input) output))
+
+(defn make-parents
+ "Creates all parent directories of file."
+ [^File file]
+ (.mkdirs (.getParentFile file)))
+
+(defmulti
+ ^{:doc "Converts argument into a Java byte array. Argument may be
+ a String, File, InputStream, or Reader. If the argument is already
+ a byte array, returns it."
+ :arglists '([arg])}
+ to-byte-array type)
+
+(defmethod to-byte-array *byte-array-type* [x] x)
+
+(defmethod to-byte-array String [^String x]
+ (.getBytes x *default-encoding*))
+
+(defmethod to-byte-array File [^File x]
+ (with-open [input (FileInputStream. x)
+ buffer (ByteArrayOutputStream.)]
+ (copy input buffer)
+ (.toByteArray buffer)))
+
+(defmethod to-byte-array InputStream [^InputStream x]
+ (let [buffer (ByteArrayOutputStream.)]
+ (copy x buffer)
+ (.toByteArray buffer)))
+
+(defmethod to-byte-array Reader [^Reader x]
+ (.getBytes (slurp* x) *default-encoding*))
+
+(defmulti relative-path-string
+ "Interpret a String or java.io.File as a relative path string.
+ Building block for clojure.contrib.java/file."
+ {:deprecated "1.2"}
+ class)
+
+(defmethod relative-path-string String [^String s]
+ (relative-path-string (File. s)))
+
+(defmethod relative-path-string File [^File f]
+ (if (.isAbsolute f)
+ (throw (IllegalArgumentException. (str f " is not a relative path")))
+ (.getPath f)))
+
+(defmulti ^File as-file
+ "Interpret a String or a java.io.File as a File. Building block
+ for clojure.contrib.java/file, which you should prefer
+ in most cases."
+ {:deprecated "1.2"}
+ class)
+(defmethod as-file String [^String s] (File. s))
+(defmethod as-file File [f] f)
+
+(defn ^File file
+ "Returns a java.io.File from string or file args."
+ {:deprecated "1.2"}
+ ([arg]
+ (as-file arg))
+ ([parent child]
+ (File. ^File (as-file parent) ^String (relative-path-string child)))
+ ([parent child & more]
+ (reduce file (file parent child) more)))
+
+(defn delete-file
+ "Delete file f. Raise an exception if it fails unless silently is true."
+ [f & [silently]]
+ (or (.delete (file f))
+ silently
+ (throw (java.io.IOException. (str "Couldn't delete " f)))))
+
+(defn delete-file-recursively
+ "Delete file f. If it's a directory, recursively delete all its contents.
+Raise an exception if any deletion fails unless silently is true."
+ [f & [silently]]
+ (let [f (file f)]
+ (if (.isDirectory f)
+ (doseq [child (.listFiles f)]
+ (delete-file-recursively child silently)))
+ (delete-file f silently)))
+
+(defmulti
+ ^{:deprecated "1.2"
+ :doc "Coerces argument (URL, URI, or String) to a java.net.URL."
+ :arglists '([arg])}
+ as-url type)
+
+(defmethod as-url URL [x] x)
+
+(defmethod as-url URI [^URI x] (.toURL x))
+
+(defmethod as-url String [^String x] (URL. x))
+
+(defmethod as-url File [^File x] (.toURL x))