comparison src/clojure/contrib/io.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 ;;; io.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.BufferedWriter,
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: Most functions here are deprecated. Use
30 ;; clojure.java.io
31 ;;
32 ;; May 13, 2009: added functions to open writers for appending
33 ;;
34 ;; May 3, 2009: renamed file to file-str, for compatibility with
35 ;; clojure.contrib.java. reader/writer no longer use this
36 ;; function.
37 ;;
38 ;; February 16, 2009: (lazy branch) fixed read-lines to work with lazy
39 ;; Clojure.
40 ;;
41 ;; January 10, 2009: added *default-encoding*, so streams are always
42 ;; opened as UTF-8.
43 ;;
44 ;; December 19, 2008: rewrote reader and writer as multimethods; added
45 ;; slurp*, file, and read-lines
46 ;;
47 ;; April 8, 2008: first version
48
49
50
51 (ns
52 ^{:author "Stuart Sierra",
53 :doc "This file defines polymorphic I/O utility functions for Clojure.
54
55 The Streams protocol defines reader, writer, input-stream and
56 output-stream methods that return BufferedReader, BufferedWriter,
57 BufferedInputStream and BufferedOutputStream instances (respectively),
58 with default implementations extended to a variety of argument
59 types: URLs or filenames as strings, java.io.File's, Sockets, etc."}
60 clojure.contrib.io
61 (:refer-clojure :exclude (spit))
62 (:import
63 (java.io Reader InputStream InputStreamReader PushbackReader
64 BufferedReader File OutputStream
65 OutputStreamWriter BufferedWriter Writer
66 FileInputStream FileOutputStream ByteArrayOutputStream
67 StringReader ByteArrayInputStream
68 BufferedInputStream BufferedOutputStream
69 CharArrayReader)
70 (java.net URI URL MalformedURLException Socket)))
71
72
73 (def
74 ^{:doc "Name of the default encoding to use when reading & writing.
75 Default is UTF-8."
76 :tag "java.lang.String"}
77 *default-encoding* "UTF-8")
78
79 (def
80 ^{:doc "Size, in bytes or characters, of the buffer used when
81 copying streams."}
82 *buffer-size* 1024)
83
84 (def
85 ^{:doc "Type object for a Java primitive byte array."}
86 *byte-array-type* (class (make-array Byte/TYPE 0)))
87
88 (def
89 ^{:doc "Type object for a Java primitive char array."}
90 *char-array-type* (class (make-array Character/TYPE 0)))
91
92
93 (defn ^File file-str
94 "Concatenates args as strings and returns a java.io.File. Replaces
95 all / and \\ with File/separatorChar. Replaces ~ at the start of
96 the path with the user.home system property."
97 [& args]
98 (let [^String s (apply str args)
99 s (.replace s \\ File/separatorChar)
100 s (.replace s \/ File/separatorChar)
101 s (if (.startsWith s "~")
102 (str (System/getProperty "user.home")
103 File/separator (subs s 1))
104 s)]
105 (File. s)))
106
107 (def
108 ^{:doc "If true, writer, output-stream and spit will open files in append mode.
109 Defaults to false. Instead of binding this var directly, use append-writer,
110 append-output-stream or append-spit."
111 :tag "java.lang.Boolean"}
112 *append* false)
113
114 (defn- assert-not-appending []
115 (when *append*
116 (throw (Exception. "Cannot change an open stream to append mode."))))
117
118 ;; @todo -- Both simple and elaborate methods for controlling buffering of
119 ;; in the Streams protocol were implemented, considered, and postponed
120 ;; see http://groups.google.com/group/clojure-dev/browse_frm/thread/3e39e9b3982f542b
121 (defprotocol Streams
122 (reader [x]
123 "Attempts to coerce its argument into an open java.io.Reader.
124 The default implementations of this protocol always return a
125 java.io.BufferedReader.
126
127 Default implementations are provided for Reader, BufferedReader,
128 InputStream, File, URI, URL, Socket, byte arrays, character arrays,
129 and String.
130
131 If argument is a String, it tries to resolve it first as a URI, then
132 as a local file name. URIs with a 'file' protocol are converted to
133 local file names. If this fails, a final attempt is made to resolve
134 the string as a resource on the CLASSPATH.
135
136 Uses *default-encoding* as the text encoding.
137
138 Should be used inside with-open to ensure the Reader is properly
139 closed.")
140 (writer [x]
141 "Attempts to coerce its argument into an open java.io.Writer.
142 The default implementations of this protocol always return a
143 java.io.BufferedWriter.
144
145 Default implementations are provided for Writer, BufferedWriter,
146 OutputStream, File, URI, URL, Socket, and String.
147
148 If the argument is a String, it tries to resolve it first as a URI, then
149 as a local file name. URIs with a 'file' protocol are converted to
150 local file names.
151
152 Should be used inside with-open to ensure the Writer is properly
153 closed.")
154 (input-stream [x]
155 "Attempts to coerce its argument into an open java.io.InputStream.
156 The default implementations of this protocol always return a
157 java.io.BufferedInputStream.
158
159 Default implementations are defined for OutputStream, File, URI, URL,
160 Socket, byte array, and String arguments.
161
162 If the argument is a String, it tries to resolve it first as a URI, then
163 as a local file name. URIs with a 'file' protocol are converted to
164 local file names.
165
166 Should be used inside with-open to ensure the InputStream is properly
167 closed.")
168 (output-stream [x]
169 "Attempts to coerce its argument into an open java.io.OutputStream.
170 The default implementations of this protocol always return a
171 java.io.BufferedOutputStream.
172
173 Default implementations are defined for OutputStream, File, URI, URL,
174 Socket, and String arguments.
175
176 If the argument is a String, it tries to resolve it first as a URI, then
177 as a local file name. URIs with a 'file' protocol are converted to
178 local file names.
179
180 Should be used inside with-open to ensure the OutputStream is
181 properly closed."))
182
183 (def default-streams-impl
184 {:reader #(reader (input-stream %))
185 :writer #(writer (output-stream %))
186 :input-stream #(throw (Exception. (str "Cannot open <" (pr-str %) "> as an InputStream.")))
187 :output-stream #(throw (Exception. (str "Cannot open <" (pr-str %) "> as an OutputStream.")))})
188
189 (extend File
190 Streams
191 (assoc default-streams-impl
192 :input-stream #(input-stream (FileInputStream. ^File %))
193 :output-stream #(let [stream (FileOutputStream. ^File % *append*)]
194 (binding [*append* false]
195 (output-stream stream)))))
196 (extend URL
197 Streams
198 (assoc default-streams-impl
199 :input-stream (fn [^URL x]
200 (input-stream (if (= "file" (.getProtocol x))
201 (FileInputStream. (.getPath x))
202 (.openStream x))))
203 :output-stream (fn [^URL x]
204 (if (= "file" (.getProtocol x))
205 (output-stream (File. (.getPath x)))
206 (throw (Exception. (str "Can not write to non-file URL <" x ">")))))))
207 (extend URI
208 Streams
209 (assoc default-streams-impl
210 :input-stream #(input-stream (.toURL ^URI %))
211 :output-stream #(output-stream (.toURL ^URI %))))
212 (extend String
213 Streams
214 (assoc default-streams-impl
215 :input-stream #(try
216 (input-stream (URL. %))
217 (catch MalformedURLException e
218 (input-stream (File. ^String %))))
219 :output-stream #(try
220 (output-stream (URL. %))
221 (catch MalformedURLException err
222 (output-stream (File. ^String %))))))
223 (extend Socket
224 Streams
225 (assoc default-streams-impl
226 :input-stream #(.getInputStream ^Socket %)
227 :output-stream #(output-stream (.getOutputStream ^Socket %))))
228 (extend *byte-array-type*
229 Streams
230 (assoc default-streams-impl :input-stream #(input-stream (ByteArrayInputStream. %))))
231 (extend *char-array-type*
232 Streams
233 (assoc default-streams-impl :reader #(reader (CharArrayReader. %))))
234 (extend Object
235 Streams
236 default-streams-impl)
237
238 (extend Reader
239 Streams
240 (assoc default-streams-impl :reader #(BufferedReader. %)))
241 (extend BufferedReader
242 Streams
243 (assoc default-streams-impl :reader identity))
244 (defn- inputstream->reader
245 [^InputStream is]
246 (reader (InputStreamReader. is *default-encoding*)))
247 (extend InputStream
248 Streams
249 (assoc default-streams-impl :input-stream #(BufferedInputStream. %)
250 :reader inputstream->reader))
251 (extend BufferedInputStream
252 Streams
253 (assoc default-streams-impl
254 :input-stream identity
255 :reader inputstream->reader))
256
257 (extend Writer
258 Streams
259 (assoc default-streams-impl :writer #(do (assert-not-appending)
260 (BufferedWriter. %))))
261 (extend BufferedWriter
262 Streams
263 (assoc default-streams-impl :writer #(do (assert-not-appending) %)))
264 (defn- outputstream->writer
265 [^OutputStream os]
266 (assert-not-appending)
267 (writer (OutputStreamWriter. os *default-encoding*)))
268 (extend OutputStream
269 Streams
270 (assoc default-streams-impl
271 :output-stream #(do (assert-not-appending)
272 (BufferedOutputStream. %))
273 :writer outputstream->writer))
274 (extend BufferedOutputStream
275 Streams
276 (assoc default-streams-impl
277 :output-stream #(do (assert-not-appending) %)
278 :writer outputstream->writer))
279
280 (defn append-output-stream
281 "Like output-stream but opens file for appending. Does not work on streams
282 that are already open."
283 {:deprecated "1.2"}
284 [x]
285 (binding [*append* true]
286 (output-stream x)))
287
288 (defn append-writer
289 "Like writer but opens file for appending. Does not work on streams
290 that are already open."
291 {:deprecated "1.2"}
292 [x]
293 (binding [*append* true]
294 (writer x)))
295
296 (defn write-lines
297 "Writes lines (a seq) to f, separated by newlines. f is opened with
298 writer, and automatically closed at the end of the sequence."
299 [f lines]
300 (with-open [^BufferedWriter writer (writer f)]
301 (loop [lines lines]
302 (when-let [line (first lines)]
303 (.write writer (str line))
304 (.newLine writer)
305 (recur (rest lines))))))
306
307 (defn read-lines
308 "Like clojure.core/line-seq but opens f with reader. Automatically
309 closes the reader AFTER YOU CONSUME THE ENTIRE SEQUENCE."
310 [f]
311 (let [read-line (fn this [^BufferedReader rdr]
312 (lazy-seq
313 (if-let [line (.readLine rdr)]
314 (cons line (this rdr))
315 (.close rdr))))]
316 (read-line (reader f))))
317
318 (defn ^String slurp*
319 "Like clojure.core/slurp but opens f with reader."
320 {:deprecated "1.2"}
321 [f]
322 (with-open [^BufferedReader r (reader f)]
323 (let [sb (StringBuilder.)]
324 (loop [c (.read r)]
325 (if (neg? c)
326 (str sb)
327 (do (.append sb (char c))
328 (recur (.read r))))))))
329
330 (defn spit
331 "Opposite of slurp. Opens f with writer, writes content, then
332 closes f."
333 {:deprecated "1.2"}
334 [f content]
335 (with-open [^Writer w (writer f)]
336 (.write w content)))
337
338 (defn append-spit
339 "Like spit but appends to file."
340 {:deprecated "1.2"}
341 [f content]
342 (with-open [^Writer w (append-writer f)]
343 (.write w content)))
344
345 (defn pwd
346 "Returns current working directory as a String. (Like UNIX 'pwd'.)
347 Note: In Java, you cannot change the current working directory."
348 {:deprecated "1.2"}
349 []
350 (System/getProperty "user.dir"))
351
352 (defmacro with-out-writer
353 "Opens a writer on f, binds it to *out*, and evalutes body.
354 Anything printed within body will be written to f."
355 [f & body]
356 `(with-open [stream# (writer ~f)]
357 (binding [*out* stream#]
358 ~@body)))
359
360 (defmacro with-out-append-writer
361 "Like with-out-writer but appends to file."
362 {:deprecated "1.2"}
363 [f & body]
364 `(with-open [stream# (append-writer ~f)]
365 (binding [*out* stream#]
366 ~@body)))
367
368 (defmacro with-in-reader
369 "Opens a PushbackReader on f, binds it to *in*, and evaluates body."
370 [f & body]
371 `(with-open [stream# (PushbackReader. (reader ~f))]
372 (binding [*in* stream#]
373 ~@body)))
374
375 (defmulti
376 ^{:deprecated "1.2"
377 :doc "Copies input to output. Returns nil.
378 Input may be an InputStream, Reader, File, byte[], or String.
379 Output may be an OutputStream, Writer, or File.
380
381 Does not close any streams except those it opens itself
382 (on a File).
383
384 Writing a File fails if the parent directory does not exist."
385 :arglists '([input output])}
386 copy
387 (fn [input output] [(type input) (type output)]))
388
389 (defmethod copy [InputStream OutputStream] [^InputStream input ^OutputStream output]
390 (let [buffer (make-array Byte/TYPE *buffer-size*)]
391 (loop []
392 (let [size (.read input buffer)]
393 (when (pos? size)
394 (do (.write output buffer 0 size)
395 (recur)))))))
396
397 (defmethod copy [InputStream Writer] [^InputStream input ^Writer output]
398 (let [^"[B" buffer (make-array Byte/TYPE *buffer-size*)]
399 (loop []
400 (let [size (.read input buffer)]
401 (when (pos? size)
402 (let [chars (.toCharArray (String. buffer 0 size *default-encoding*))]
403 (do (.write output chars)
404 (recur))))))))
405
406 (defmethod copy [InputStream File] [^InputStream input ^File output]
407 (with-open [out (FileOutputStream. output)]
408 (copy input out)))
409
410 (defmethod copy [Reader OutputStream] [^Reader input ^OutputStream output]
411 (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)]
412 (loop []
413 (let [size (.read input buffer)]
414 (when (pos? size)
415 (let [bytes (.getBytes (String. buffer 0 size) *default-encoding*)]
416 (do (.write output bytes)
417 (recur))))))))
418
419 (defmethod copy [Reader Writer] [^Reader input ^Writer output]
420 (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)]
421 (loop []
422 (let [size (.read input buffer)]
423 (when (pos? size)
424 (do (.write output buffer 0 size)
425 (recur)))))))
426
427 (defmethod copy [Reader File] [^Reader input ^File output]
428 (with-open [out (FileOutputStream. output)]
429 (copy input out)))
430
431 (defmethod copy [File OutputStream] [^File input ^OutputStream output]
432 (with-open [in (FileInputStream. input)]
433 (copy in output)))
434
435 (defmethod copy [File Writer] [^File input ^Writer output]
436 (with-open [in (FileInputStream. input)]
437 (copy in output)))
438
439 (defmethod copy [File File] [^File input ^File output]
440 (with-open [in (FileInputStream. input)
441 out (FileOutputStream. output)]
442 (copy in out)))
443
444 (defmethod copy [String OutputStream] [^String input ^OutputStream output]
445 (copy (StringReader. input) output))
446
447 (defmethod copy [String Writer] [^String input ^Writer output]
448 (copy (StringReader. input) output))
449
450 (defmethod copy [String File] [^String input ^File output]
451 (copy (StringReader. input) output))
452
453 (defmethod copy [*char-array-type* OutputStream] [input ^OutputStream output]
454 (copy (CharArrayReader. input) output))
455
456 (defmethod copy [*char-array-type* Writer] [input ^Writer output]
457 (copy (CharArrayReader. input) output))
458
459 (defmethod copy [*char-array-type* File] [input ^File output]
460 (copy (CharArrayReader. input) output))
461
462 (defmethod copy [*byte-array-type* OutputStream] [^"[B" input ^OutputStream output]
463 (copy (ByteArrayInputStream. input) output))
464
465 (defmethod copy [*byte-array-type* Writer] [^"[B" input ^Writer output]
466 (copy (ByteArrayInputStream. input) output))
467
468 (defmethod copy [*byte-array-type* File] [^"[B" input ^Writer output]
469 (copy (ByteArrayInputStream. input) output))
470
471 (defn make-parents
472 "Creates all parent directories of file."
473 [^File file]
474 (.mkdirs (.getParentFile file)))
475
476 (defmulti
477 ^{:doc "Converts argument into a Java byte array. Argument may be
478 a String, File, InputStream, or Reader. If the argument is already
479 a byte array, returns it."
480 :arglists '([arg])}
481 to-byte-array type)
482
483 (defmethod to-byte-array *byte-array-type* [x] x)
484
485 (defmethod to-byte-array String [^String x]
486 (.getBytes x *default-encoding*))
487
488 (defmethod to-byte-array File [^File x]
489 (with-open [input (FileInputStream. x)
490 buffer (ByteArrayOutputStream.)]
491 (copy input buffer)
492 (.toByteArray buffer)))
493
494 (defmethod to-byte-array InputStream [^InputStream x]
495 (let [buffer (ByteArrayOutputStream.)]
496 (copy x buffer)
497 (.toByteArray buffer)))
498
499 (defmethod to-byte-array Reader [^Reader x]
500 (.getBytes (slurp* x) *default-encoding*))
501
502 (defmulti relative-path-string
503 "Interpret a String or java.io.File as a relative path string.
504 Building block for clojure.contrib.java/file."
505 {:deprecated "1.2"}
506 class)
507
508 (defmethod relative-path-string String [^String s]
509 (relative-path-string (File. s)))
510
511 (defmethod relative-path-string File [^File f]
512 (if (.isAbsolute f)
513 (throw (IllegalArgumentException. (str f " is not a relative path")))
514 (.getPath f)))
515
516 (defmulti ^File as-file
517 "Interpret a String or a java.io.File as a File. Building block
518 for clojure.contrib.java/file, which you should prefer
519 in most cases."
520 {:deprecated "1.2"}
521 class)
522 (defmethod as-file String [^String s] (File. s))
523 (defmethod as-file File [f] f)
524
525 (defn ^File file
526 "Returns a java.io.File from string or file args."
527 {:deprecated "1.2"}
528 ([arg]
529 (as-file arg))
530 ([parent child]
531 (File. ^File (as-file parent) ^String (relative-path-string child)))
532 ([parent child & more]
533 (reduce file (file parent child) more)))
534
535 (defn delete-file
536 "Delete file f. Raise an exception if it fails unless silently is true."
537 [f & [silently]]
538 (or (.delete (file f))
539 silently
540 (throw (java.io.IOException. (str "Couldn't delete " f)))))
541
542 (defn delete-file-recursively
543 "Delete file f. If it's a directory, recursively delete all its contents.
544 Raise an exception if any deletion fails unless silently is true."
545 [f & [silently]]
546 (let [f (file f)]
547 (if (.isDirectory f)
548 (doseq [child (.listFiles f)]
549 (delete-file-recursively child silently)))
550 (delete-file f silently)))
551
552 (defmulti
553 ^{:deprecated "1.2"
554 :doc "Coerces argument (URL, URI, or String) to a java.net.URL."
555 :arglists '([arg])}
556 as-url type)
557
558 (defmethod as-url URL [x] x)
559
560 (defmethod as-url URI [^URI x] (.toURL x))
561
562 (defmethod as-url String [^String x] (URL. x))
563
564 (defmethod as-url File [^File x] (.toURL x))