comparison 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
comparison
equal deleted inserted replaced
9:35cf337adfcf 10:ef7dbbd6452c
1 ;;; duck_streams.clj -- duck-typed I/O streams for Clojure
2
3 ;; by Stuart Sierra, http://stuartsierra.com/
4 ;; May 13, 2009
5
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.
13
14
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.
25
26
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
47
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.
57
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)))
70
71
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")
77
78 (def
79 ^{:doc "Size, in bytes or characters, of the buffer used when
80 copying streams."}
81 *buffer-size* 1024)
82
83 (def
84 ^{:doc "Type object for a Java primitive byte array."}
85 *byte-array-type* (class (make-array Byte/TYPE 0)))
86
87
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)))
100
101
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.
106
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.
110
111 Should be used inside with-open to ensure the Reader is properly
112 closed."
113 :arglists '([x])}
114 reader class)
115
116 (defmethod reader Reader [x]
117 (BufferedReader. x))
118
119 (defmethod reader InputStream [^InputStream x]
120 (BufferedReader. (InputStreamReader. x *default-encoding*)))
121
122 (defmethod reader File [^File x]
123 (reader (FileInputStream. x)))
124
125 (defmethod reader URL [^URL x]
126 (reader (if (= "file" (.getProtocol x))
127 (FileInputStream. (.getPath x))
128 (.openStream x))))
129
130 (defmethod reader URI [^URI x]
131 (reader (.toURL x)))
132
133 (defmethod reader String [^String x]
134 (try (let [url (URL. x)]
135 (reader url))
136 (catch MalformedURLException e
137 (reader (File. x)))))
138
139 (defmethod reader Socket [^Socket x]
140 (reader (.getInputStream x)))
141
142 (defmethod reader :default [x]
143 (throw (Exception. (str "Cannot open " (pr-str x) " as a reader."))))
144
145
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)
151
152
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.
158
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.
162
163 Should be used inside with-open to ensure the Writer is properly
164 closed."
165 :arglists '([x])}
166 writer class)
167
168 (defn- assert-not-appending []
169 (when *append-to-writer*
170 (throw (Exception. "Cannot change an open stream to append mode."))))
171
172 (defmethod writer PrintWriter [x]
173 (assert-not-appending)
174 x)
175
176 (defmethod writer BufferedWriter [^BufferedWriter x]
177 (assert-not-appending)
178 (PrintWriter. x))
179
180 (defmethod writer Writer [x]
181 (assert-not-appending)
182 ;; Writer includes sub-classes such as FileWriter
183 (PrintWriter. (BufferedWriter. x)))
184
185 (defmethod writer OutputStream [^OutputStream x]
186 (assert-not-appending)
187 (PrintWriter.
188 (BufferedWriter.
189 (OutputStreamWriter. x *default-encoding*))))
190
191 (defmethod writer File [^File x]
192 (let [stream (FileOutputStream. x *append-to-writer*)]
193 (binding [*append-to-writer* false]
194 (writer stream))))
195
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 ">")))))
200
201 (defmethod writer URI [^URI x]
202 (writer (.toURL x)))
203
204 (defmethod writer String [^String x]
205 (try (let [url (URL. x)]
206 (writer url))
207 (catch MalformedURLException err
208 (writer (File. x)))))
209
210 (defmethod writer Socket [^Socket x]
211 (writer (.getOutputStream x)))
212
213 (defmethod writer :default [x]
214 (throw (Exception. (str "Cannot open <" (pr-str x) "> as a writer."))))
215
216
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)))
223
224
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))))))
235
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))))
246
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))))))))
257
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)))
264
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)))
270
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"))
276
277
278
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)))
286
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)))
293
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)))
300
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.
305
306 Does not close any streams except those it opens itself
307 (on a File).
308
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)]))
313
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)))))))
321
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))))))))
330
331 (defmethod copy [InputStream File] [^InputStream input ^File output]
332 (with-open [out (FileOutputStream. output)]
333 (copy input out)))
334
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))))))))
343
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)))))))
351
352 (defmethod copy [Reader File] [^Reader input ^File output]
353 (with-open [out (FileOutputStream. output)]
354 (copy input out)))
355
356 (defmethod copy [File OutputStream] [^File input ^OutputStream output]
357 (with-open [in (FileInputStream. input)]
358 (copy in output)))
359
360 (defmethod copy [File Writer] [^File input ^Writer output]
361 (with-open [in (FileInputStream. input)]
362 (copy in output)))
363
364 (defmethod copy [File File] [^File input ^File output]
365 (with-open [in (FileInputStream. input)
366 out (FileOutputStream. output)]
367 (copy in out)))
368
369 (defmethod copy [String OutputStream] [^String input ^OutputStream output]
370 (copy (StringReader. input) output))
371
372 (defmethod copy [String Writer] [^String input ^Writer output]
373 (copy (StringReader. input) output))
374
375 (defmethod copy [String File] [^String input ^File output]
376 (copy (StringReader. input) output))
377
378 (defmethod copy [*byte-array-type* OutputStream] [^"[B" input ^OutputStream output]
379 (copy (ByteArrayInputStream. input) output))
380
381 (defmethod copy [*byte-array-type* Writer] [^"[B" input ^Writer output]
382 (copy (ByteArrayInputStream. input) output))
383
384 (defmethod copy [*byte-array-type* File] [^"[B" input ^Writer output]
385 (copy (ByteArrayInputStream. input) output))
386
387
388 (defn make-parents
389 "Creates all parent directories of file."
390 [^File file]
391 (.mkdirs (.getParentFile file)))
392
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)
399
400 (defmethod to-byte-array *byte-array-type* [x] x)
401
402 (defmethod to-byte-array String [^String x]
403 (.getBytes x *default-encoding*))
404
405 (defmethod to-byte-array File [^File x]
406 (with-open [input (FileInputStream. x)
407 buffer (ByteArrayOutputStream.)]
408 (copy input buffer)
409 (.toByteArray buffer)))
410
411 (defmethod to-byte-array InputStream [^InputStream x]
412 (let [buffer (ByteArrayOutputStream.)]
413 (copy x buffer)
414 (.toByteArray buffer)))
415
416 (defmethod to-byte-array Reader [^Reader x]
417 (.getBytes (slurp* x) *default-encoding*))
418