diff options
Diffstat (limited to 'modules/io/src/main/clojure/clojure/contrib/io.clj')
-rw-r--r-- | modules/io/src/main/clojure/clojure/contrib/io.clj | 564 |
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)) |