diff options
-rw-r--r-- | src/main/clojure/clojure/contrib/io.clj | 384 | ||||
-rw-r--r-- | src/test/clojure/clojure/contrib/test_io.clj | 31 |
2 files changed, 224 insertions, 191 deletions
diff --git a/src/main/clojure/clojure/contrib/io.clj b/src/main/clojure/clojure/contrib/io.clj index 7101947c..7ae69036 100644 --- a/src/main/clojure/clojure/contrib/io.clj +++ b/src/main/clojure/clojure/contrib/io.clj @@ -14,7 +14,7 @@ ;; 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.PrintWriter, +;; 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. @@ -47,24 +47,22 @@ (ns #^{:author "Stuart Sierra", - :doc "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.PrintWriter, - 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."} + :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 (:import (java.io Reader InputStream InputStreamReader PushbackReader - BufferedReader File PrintWriter OutputStream + BufferedReader File OutputStream OutputStreamWriter BufferedWriter Writer FileInputStream FileOutputStream ByteArrayOutputStream StringReader ByteArrayInputStream - BufferedInputStream BufferedOutputStream) + BufferedInputStream BufferedOutputStream + CharArrayReader) (java.net URI URL MalformedURLException Socket))) @@ -83,6 +81,10 @@ #^{: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 @@ -98,86 +100,10 @@ s)] (File. s))) - -(defmulti #^{:tag BufferedInputStream - :doc "Attempts to coerce its argument into an open - java.io.BufferedInputStream. Argument may be an instance of - BufferedInputStream, InputStream, File, URI, URL, Socket, or 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. - - Should be used inside with-open to ensure the InputStream is properly - closed." - :arglists '([x])} - input-stream class) - -(defmethod input-stream BufferedInputStream [x] - x) - -(defmethod input-stream InputStream [x] - (BufferedInputStream. x)) - -(defmethod input-stream File [#^File x] - (input-stream (FileInputStream. x))) - -(defmethod input-stream URL [#^URL x] - (input-stream (if (= "file" (.getProtocol x)) - (FileInputStream. (.getPath x)) - (.openStream x)))) - -(defmethod input-stream URI [#^URI x] - (input-stream (.toURL x))) - -(defmethod input-stream String [#^String x] - (try (let [url (URL. x)] - (input-stream url)) - (catch MalformedURLException e - (input-stream (File. x))))) - -(defmethod input-stream Socket [#^Socket x] - (input-stream (.getInputStream x))) - -(defmethod input-stream :default [x] - (throw (Exception. (str "Cannot open " (pr-str x) " as an InputStream.")))) - - -(defmulti #^{:tag BufferedReader - :doc "Attempts to coerce its argument into an open - java.io.BufferedReader. Argument may be an instance of Reader, - BufferedReader, InputStream, File, URI, URL, Socket, or 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." - :arglists '([x])} - reader class) - -(defmethod reader BufferedReader [x] - x) - -(defmethod reader Reader [x] - (BufferedReader. x)) - -(defmethod reader InputStream [#^InputStream x] - (reader (InputStreamReader. x *default-encoding*))) - -(defmethod reader :default [x] - ; input-stream throws if it can't hanlde x. - (reader (input-stream x))) - (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." + Defaults to false. Instead of binding this var directly, use append-writer, + append-output-stream or append-spit." :tag "java.lang.Boolean"} *append* false) @@ -185,52 +111,167 @@ (when *append* (throw (Exception. "Cannot change an open stream to append mode.")))) -(defmulti #^{:tag OutputStream - :doc "Attempts to coerce its argument into an open - java.io.OutputStream or java.io.BufferedOutputStream. Argument may - be an instance of OutputStream, File, URI, URL, Socket, or 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. - - Should be used inside with-open to ensure the OutputStream is - properly closed." - :arglists '([x])} - output-stream class) - -(defmethod output-stream BufferedOutputStream [#^BufferedOutputStream x] +;; @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) - x) - -(defmethod output-stream OutputStream [#^OutputStream x] - (assert-not-appending) - (BufferedOutputStream. x)) - -(defmethod output-stream File [#^File x] - (let [stream (FileOutputStream. x *append*)] - (binding [*append* false] - (output-stream stream)))) - -(defmethod output-stream URL [#^URL x] - (if (= "file" (.getProtocol x)) - (output-stream (File. (.getPath x))) - (throw (Exception. (str "Can not write to non-file URL <" x ">"))))) - -(defmethod output-stream URI [#^URI x] - (output-stream (.toURL x))) - -(defmethod output-stream String [#^String x] - (try (let [url (URL. x)] - (output-stream url)) - (catch MalformedURLException err - (output-stream (File. x))))) - -(defmethod output-stream Socket [#^Socket x] - (output-stream (.getOutputStream x))) - -(defmethod output-stream :default [x] - (throw (Exception. (str "Cannot open <" (pr-str x) "> as an output stream.")))) + (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 @@ -239,53 +280,6 @@ (binding [*append* true] (output-stream x))) - -(defmulti #^{:tag PrintWriter - :doc "Attempts to coerce its argument into an open java.io.PrintWriter - wrapped around a java.io.BufferedWriter. Argument may be an - instance of Writer, PrintWriter, BufferedWriter, OutputStream, File, - URI, URL, Socket, or 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. - - Should be used inside with-open to ensure the Writer is properly - closed." - :arglists '([x])} - writer class) - -(defmethod writer PrintWriter [x] - (assert-not-appending) - x) - -(defmethod writer BufferedWriter [#^BufferedWriter x] - (assert-not-appending) - (PrintWriter. x)) - -(defmethod writer Writer [x] - (assert-not-appending) - ;; Writer includes sub-classes such as FileWriter - (writer (BufferedWriter. x))) - -(defmethod writer OutputStream [#^OutputStream x] - (assert-not-appending) - (writer (OutputStreamWriter. x *default-encoding*))) - -(defmethod writer File [#^File x] - (let [stream (FileOutputStream. x *append*)] - (binding [*append* false] - (writer stream)))) - -(defmethod writer String [#^String x] - (try (let [url (URL. x)] - (writer url)) - (catch MalformedURLException err - (writer (File. x))))) - -(defmethod writer :default [x] - (writer (output-stream x))) - (defn append-writer "Like writer but opens file for appending. Does not work on streams that are already open." @@ -293,16 +287,15 @@ (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 [#^PrintWriter writer (writer f)] + (with-open [#^BufferedWriter writer (writer f)] (loop [lines lines] (when-let [line (first lines)] (.write writer (str line)) - (.println writer) + (.newLine writer) (recur (rest lines)))))) (defn read-lines @@ -331,14 +324,14 @@ "Opposite of slurp. Opens f with writer, writes content, then closes f." [f content] - (with-open [#^PrintWriter w (writer f)] - (.print w content))) + (with-open [#^Writer w (writer f)] + (.write w content))) (defn append-spit "Like spit but appends to file." [f content] - (with-open [#^PrintWriter w (append-writer f)] - (.print w content))) + (with-open [#^Writer w (append-writer f)] + (.write w content))) (defn pwd "Returns current working directory as a String. (Like UNIX 'pwd'.) @@ -447,6 +440,15 @@ (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)) @@ -546,4 +548,4 @@ Raise an exception if any deletion fails unless silently is true." (defmethod as-url String [#^String x] (URL. x)) -(defmethod as-url File [#^File x] (.toURL x)) +(defmethod as-url File [#^File x] (.toURL x))
\ No newline at end of file diff --git a/src/test/clojure/clojure/contrib/test_io.clj b/src/test/clojure/clojure/contrib/test_io.clj index 32d4909a..e4e1d1e2 100644 --- a/src/test/clojure/clojure/contrib/test_io.clj +++ b/src/test/clojure/clojure/contrib/test_io.clj @@ -62,3 +62,34 @@ [(.. file toString) "File as String"]]] (with-open [s (input-stream expr)] (stream-should-have s bytes msg))))) + +(deftest test-streams-buffering + (let [data (.getBytes "")] + (is (instance? java.io.BufferedReader (reader data))) + (is (instance? java.io.BufferedWriter (writer (java.io.ByteArrayOutputStream.)))) + (is (instance? java.io.BufferedInputStream (input-stream data))) + (is (instance? java.io.BufferedOutputStream (output-stream (java.io.ByteArrayOutputStream.)))))) + +(deftest test-streams-defaults + (let [f (File/createTempFile "clojure.contrib" "test-reader-writer") + content "test\u2099ing"] + (try + (is (thrown? Exception (reader (Object.)))) + (is (thrown? Exception (writer (Object.)))) + + (are [write-to read-from] (= content (do + (spit write-to content) + (slurp* (or read-from write-to)))) + f nil + (.getAbsolutePath f) nil + (.toURL f) nil + (.toURI f) nil + (java.io.FileOutputStream. f) f + (java.io.OutputStreamWriter. (java.io.FileOutputStream. f) "UTF-8") f + f (java.io.FileInputStream. f) + f (java.io.InputStreamReader. (java.io.FileInputStream. f) "UTF-8")) + + (is (= content (slurp* (.getBytes content "UTF-8")))) + (is (= content (slurp* (.toCharArray content)))) + (finally + (.delete f)))))
\ No newline at end of file |