view src/clojure/contrib/duck_streams.clj @ 10:ef7dbbd6452c

added clojure source goodness
author Robert McIntyre <rlm@mit.edu>
date Sat, 21 Aug 2010 06:25:44 -0400
parents
children
line wrap: on
line source
1 ;;; duck_streams.clj -- duck-typed I/O streams for Clojure
3 ;; by Stuart Sierra, http://stuartsierra.com/
4 ;; May 13, 2009
6 ;; Copyright (c) Stuart Sierra, 2009. All rights reserved. The use
7 ;; and distribution terms for this software are covered by the Eclipse
8 ;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
9 ;; which can be found in the file epl-v10.html at the root of this
10 ;; distribution. By using this software in any fashion, you are
11 ;; agreeing to be bound by the terms of this license. You must not
12 ;; remove this notice, or any other, from this software.
15 ;; This file defines "duck-typed" I/O utility functions for Clojure.
16 ;; The 'reader' and 'writer' functions will open and return an
17 ;; instance of java.io.BufferedReader and java.io.PrintWriter,
18 ;; respectively, for a variety of argument types -- filenames as
19 ;; strings, URLs, java.io.File's, etc. 'reader' even works on http
20 ;; URLs.
21 ;;
22 ;; Note: this is not really "duck typing" as implemented in languages
23 ;; like Ruby. A better name would have been "do-what-I-mean-streams"
24 ;; or "just-give-me-a-stream", but ducks are funnier.
27 ;; CHANGE LOG
28 ;;
29 ;; July 23, 2010: DEPRECATED in 1.2. Use clojure.java.io instead.
30 ;;
31 ;; May 13, 2009: added functions to open writers for appending
32 ;;
33 ;; May 3, 2009: renamed file to file-str, for compatibility with
34 ;; clojure.contrib.java-utils. reader/writer no longer use this
35 ;; function.
36 ;;
37 ;; February 16, 2009: (lazy branch) fixed read-lines to work with lazy
38 ;; Clojure.
39 ;;
40 ;; January 10, 2009: added *default-encoding*, so streams are always
41 ;; opened as UTF-8.
42 ;;
43 ;; December 19, 2008: rewrote reader and writer as multimethods; added
44 ;; slurp*, file, and read-lines
45 ;;
46 ;; April 8, 2008: first version
48 (ns
49 ^{:author "Stuart Sierra",
50 :deprecated "1.2"
51 :doc "This file defines \"duck-typed\" I/O utility functions for Clojure.
52 The 'reader' and 'writer' functions will open and return an
53 instance of java.io.BufferedReader and java.io.PrintWriter,
54 respectively, for a variety of argument types -- filenames as
55 strings, URLs, java.io.File's, etc. 'reader' even works on http
56 URLs.
58 Note: this is not really \"duck typing\" as implemented in languages
59 like Ruby. A better name would have been \"do-what-I-mean-streams\"
60 or \"just-give-me-a-stream\", but ducks are funnier."}
61 clojure.contrib.duck-streams
62 (:refer-clojure :exclude (spit))
63 (:import
64 (java.io Reader InputStream InputStreamReader PushbackReader
65 BufferedReader File PrintWriter OutputStream
66 OutputStreamWriter BufferedWriter Writer
67 FileInputStream FileOutputStream ByteArrayOutputStream
68 StringReader ByteArrayInputStream)
69 (java.net URI URL MalformedURLException Socket)))
72 (def
73 ^{:doc "Name of the default encoding to use when reading & writing.
74 Default is UTF-8."
75 :tag "java.lang.String"}
76 *default-encoding* "UTF-8")
78 (def
79 ^{:doc "Size, in bytes or characters, of the buffer used when
80 copying streams."}
81 *buffer-size* 1024)
83 (def
84 ^{:doc "Type object for a Java primitive byte array."}
85 *byte-array-type* (class (make-array Byte/TYPE 0)))
88 (defn ^File file-str
89 "Concatenates args as strings and returns a java.io.File. Replaces
90 all / and \\ with File/separatorChar. Replaces ~ at the start of
91 the path with the user.home system property."
92 [& args]
93 (let [^String s (apply str args)
94 s (.replaceAll (re-matcher #"[/\\]" s) File/separator)
95 s (if (.startsWith s "~")
96 (str (System/getProperty "user.home")
97 File/separator (subs s 1))
98 s)]
99 (File. s)))
102 (defmulti ^{:tag BufferedReader
103 :doc "Attempts to coerce its argument into an open
104 java.io.BufferedReader. Argument may be an instance of Reader,
105 BufferedReader, InputStream, File, URI, URL, Socket, or String.
107 If argument is a String, it tries to resolve it first as a URI, then
108 as a local file name. URIs with a 'file' protocol are converted to
109 local file names. Uses *default-encoding* as the text encoding.
111 Should be used inside with-open to ensure the Reader is properly
112 closed."
113 :arglists '([x])}
114 reader class)
116 (defmethod reader Reader [x]
117 (BufferedReader. x))
119 (defmethod reader InputStream [^InputStream x]
120 (BufferedReader. (InputStreamReader. x *default-encoding*)))
122 (defmethod reader File [^File x]
123 (reader (FileInputStream. x)))
125 (defmethod reader URL [^URL x]
126 (reader (if (= "file" (.getProtocol x))
127 (FileInputStream. (.getPath x))
128 (.openStream x))))
130 (defmethod reader URI [^URI x]
131 (reader (.toURL x)))
133 (defmethod reader String [^String x]
134 (try (let [url (URL. x)]
135 (reader url))
136 (catch MalformedURLException e
137 (reader (File. x)))))
139 (defmethod reader Socket [^Socket x]
140 (reader (.getInputStream x)))
142 (defmethod reader :default [x]
143 (throw (Exception. (str "Cannot open " (pr-str x) " as a reader."))))
146 (def
147 ^{:doc "If true, writer and spit will open files in append mode.
148 Defaults to false. Use append-writer or append-spit."
149 :tag "java.lang.Boolean"}
150 *append-to-writer* false)
153 (defmulti ^{:tag PrintWriter
154 :doc "Attempts to coerce its argument into an open java.io.PrintWriter
155 wrapped around a java.io.BufferedWriter. Argument may be an
156 instance of Writer, PrintWriter, BufferedWriter, OutputStream, File,
157 URI, URL, Socket, or String.
159 If argument is a String, it tries to resolve it first as a URI, then
160 as a local file name. URIs with a 'file' protocol are converted to
161 local file names.
163 Should be used inside with-open to ensure the Writer is properly
164 closed."
165 :arglists '([x])}
166 writer class)
168 (defn- assert-not-appending []
169 (when *append-to-writer*
170 (throw (Exception. "Cannot change an open stream to append mode."))))
172 (defmethod writer PrintWriter [x]
173 (assert-not-appending)
174 x)
176 (defmethod writer BufferedWriter [^BufferedWriter x]
177 (assert-not-appending)
178 (PrintWriter. x))
180 (defmethod writer Writer [x]
181 (assert-not-appending)
182 ;; Writer includes sub-classes such as FileWriter
183 (PrintWriter. (BufferedWriter. x)))
185 (defmethod writer OutputStream [^OutputStream x]
186 (assert-not-appending)
187 (PrintWriter.
188 (BufferedWriter.
189 (OutputStreamWriter. x *default-encoding*))))
191 (defmethod writer File [^File x]
192 (let [stream (FileOutputStream. x *append-to-writer*)]
193 (binding [*append-to-writer* false]
194 (writer stream))))
196 (defmethod writer URL [^URL x]
197 (if (= "file" (.getProtocol x))
198 (writer (File. (.getPath x)))
199 (throw (Exception. (str "Cannot write to non-file URL <" x ">")))))
201 (defmethod writer URI [^URI x]
202 (writer (.toURL x)))
204 (defmethod writer String [^String x]
205 (try (let [url (URL. x)]
206 (writer url))
207 (catch MalformedURLException err
208 (writer (File. x)))))
210 (defmethod writer Socket [^Socket x]
211 (writer (.getOutputStream x)))
213 (defmethod writer :default [x]
214 (throw (Exception. (str "Cannot open <" (pr-str x) "> as a writer."))))
217 (defn append-writer
218 "Like writer but opens file for appending. Does not work on streams
219 that are already open."
220 [x]
221 (binding [*append-to-writer* true]
222 (writer x)))
225 (defn write-lines
226 "Writes lines (a seq) to f, separated by newlines. f is opened with
227 writer, and automatically closed at the end of the sequence."
228 [f lines]
229 (with-open [^PrintWriter writer (writer f)]
230 (loop [lines lines]
231 (when-let [line (first lines)]
232 (.write writer (str line))
233 (.println writer)
234 (recur (rest lines))))))
236 (defn read-lines
237 "Like clojure.core/line-seq but opens f with reader. Automatically
238 closes the reader AFTER YOU CONSUME THE ENTIRE SEQUENCE."
239 [f]
240 (let [read-line (fn this [^BufferedReader rdr]
241 (lazy-seq
242 (if-let [line (.readLine rdr)]
243 (cons line (this rdr))
244 (.close rdr))))]
245 (read-line (reader f))))
247 (defn ^String slurp*
248 "Like clojure.core/slurp but opens f with reader."
249 [f]
250 (with-open [^BufferedReader r (reader f)]
251 (let [sb (StringBuilder.)]
252 (loop [c (.read r)]
253 (if (neg? c)
254 (str sb)
255 (do (.append sb (char c))
256 (recur (.read r))))))))
258 (defn spit
259 "Opposite of slurp. Opens f with writer, writes content, then
260 closes f."
261 [f content]
262 (with-open [^PrintWriter w (writer f)]
263 (.print w content)))
265 (defn append-spit
266 "Like spit but appends to file."
267 [f content]
268 (with-open [^PrintWriter w (append-writer f)]
269 (.print w content)))
271 (defn pwd
272 "Returns current working directory as a String. (Like UNIX 'pwd'.)
273 Note: In Java, you cannot change the current working directory."
274 []
275 (System/getProperty "user.dir"))
279 (defmacro with-out-writer
280 "Opens a writer on f, binds it to *out*, and evalutes body.
281 Anything printed within body will be written to f."
282 [f & body]
283 `(with-open [stream# (writer ~f)]
284 (binding [*out* stream#]
285 ~@body)))
287 (defmacro with-out-append-writer
288 "Like with-out-writer but appends to file."
289 [f & body]
290 `(with-open [stream# (append-writer ~f)]
291 (binding [*out* stream#]
292 ~@body)))
294 (defmacro with-in-reader
295 "Opens a PushbackReader on f, binds it to *in*, and evaluates body."
296 [f & body]
297 `(with-open [stream# (PushbackReader. (reader ~f))]
298 (binding [*in* stream#]
299 ~@body)))
301 (defmulti
302 ^{:doc "Copies input to output. Returns nil.
303 Input may be an InputStream, Reader, File, byte[], or String.
304 Output may be an OutputStream, Writer, or File.
306 Does not close any streams except those it opens itself
307 (on a File).
309 Writing a File fails if the parent directory does not exist."
310 :arglists '([input output])}
311 copy
312 (fn [input output] [(type input) (type output)]))
314 (defmethod copy [InputStream OutputStream] [^InputStream input ^OutputStream output]
315 (let [buffer (make-array Byte/TYPE *buffer-size*)]
316 (loop []
317 (let [size (.read input buffer)]
318 (when (pos? size)
319 (do (.write output buffer 0 size)
320 (recur)))))))
322 (defmethod copy [InputStream Writer] [^InputStream input ^Writer output]
323 (let [^"[B" buffer (make-array Byte/TYPE *buffer-size*)]
324 (loop []
325 (let [size (.read input buffer)]
326 (when (pos? size)
327 (let [chars (.toCharArray (String. buffer 0 size *default-encoding*))]
328 (do (.write output chars)
329 (recur))))))))
331 (defmethod copy [InputStream File] [^InputStream input ^File output]
332 (with-open [out (FileOutputStream. output)]
333 (copy input out)))
335 (defmethod copy [Reader OutputStream] [^Reader input ^OutputStream output]
336 (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)]
337 (loop []
338 (let [size (.read input buffer)]
339 (when (pos? size)
340 (let [bytes (.getBytes (String. buffer 0 size) *default-encoding*)]
341 (do (.write output bytes)
342 (recur))))))))
344 (defmethod copy [Reader Writer] [^Reader input ^Writer output]
345 (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)]
346 (loop []
347 (let [size (.read input buffer)]
348 (when (pos? size)
349 (do (.write output buffer 0 size)
350 (recur)))))))
352 (defmethod copy [Reader File] [^Reader input ^File output]
353 (with-open [out (FileOutputStream. output)]
354 (copy input out)))
356 (defmethod copy [File OutputStream] [^File input ^OutputStream output]
357 (with-open [in (FileInputStream. input)]
358 (copy in output)))
360 (defmethod copy [File Writer] [^File input ^Writer output]
361 (with-open [in (FileInputStream. input)]
362 (copy in output)))
364 (defmethod copy [File File] [^File input ^File output]
365 (with-open [in (FileInputStream. input)
366 out (FileOutputStream. output)]
367 (copy in out)))
369 (defmethod copy [String OutputStream] [^String input ^OutputStream output]
370 (copy (StringReader. input) output))
372 (defmethod copy [String Writer] [^String input ^Writer output]
373 (copy (StringReader. input) output))
375 (defmethod copy [String File] [^String input ^File output]
376 (copy (StringReader. input) output))
378 (defmethod copy [*byte-array-type* OutputStream] [^"[B" input ^OutputStream output]
379 (copy (ByteArrayInputStream. input) output))
381 (defmethod copy [*byte-array-type* Writer] [^"[B" input ^Writer output]
382 (copy (ByteArrayInputStream. input) output))
384 (defmethod copy [*byte-array-type* File] [^"[B" input ^Writer output]
385 (copy (ByteArrayInputStream. input) output))
388 (defn make-parents
389 "Creates all parent directories of file."
390 [^File file]
391 (.mkdirs (.getParentFile file)))
393 (defmulti
394 ^{:doc "Converts argument into a Java byte array. Argument may be
395 a String, File, InputStream, or Reader. If the argument is already
396 a byte array, returns it."
397 :arglists '([arg])}
398 to-byte-array type)
400 (defmethod to-byte-array *byte-array-type* [x] x)
402 (defmethod to-byte-array String [^String x]
403 (.getBytes x *default-encoding*))
405 (defmethod to-byte-array File [^File x]
406 (with-open [input (FileInputStream. x)
407 buffer (ByteArrayOutputStream.)]
408 (copy input buffer)
409 (.toByteArray buffer)))
411 (defmethod to-byte-array InputStream [^InputStream x]
412 (let [buffer (ByteArrayOutputStream.)]
413 (copy x buffer)
414 (.toByteArray buffer)))
416 (defmethod to-byte-array Reader [^Reader x]
417 (.getBytes (slurp* x) *default-encoding*))