view 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
line wrap: on
line source
1 ;;; io.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.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.
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
51 (ns
52 ^{:author "Stuart Sierra",
53 :doc "This file defines polymorphic I/O utility functions for Clojure.
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)))
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")
79 (def
80 ^{:doc "Size, in bytes or characters, of the buffer used when
81 copying streams."}
82 *buffer-size* 1024)
84 (def
85 ^{:doc "Type object for a Java primitive byte array."}
86 *byte-array-type* (class (make-array Byte/TYPE 0)))
88 (def
89 ^{:doc "Type object for a Java primitive char array."}
90 *char-array-type* (class (make-array Character/TYPE 0)))
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)))
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)
114 (defn- assert-not-appending []
115 (when *append*
116 (throw (Exception. "Cannot change an open stream to append mode."))))
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.
127 Default implementations are provided for Reader, BufferedReader,
128 InputStream, File, URI, URL, Socket, byte arrays, character arrays,
129 and String.
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.
136 Uses *default-encoding* as the text encoding.
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.
145 Default implementations are provided for Writer, BufferedWriter,
146 OutputStream, File, URI, URL, Socket, and String.
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.
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.
159 Default implementations are defined for OutputStream, File, URI, URL,
160 Socket, byte array, and String arguments.
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.
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.
173 Default implementations are defined for OutputStream, File, URI, URL,
174 Socket, and String arguments.
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.
180 Should be used inside with-open to ensure the OutputStream is
181 properly closed."))
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.")))})
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)
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))
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))
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)))
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)))
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))))))
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))))
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))))))))
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)))
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)))
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"))
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)))
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)))
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)))
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.
381 Does not close any streams except those it opens itself
382 (on a File).
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)]))
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)))))))
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))))))))
406 (defmethod copy [InputStream File] [^InputStream input ^File output]
407 (with-open [out (FileOutputStream. output)]
408 (copy input out)))
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))))))))
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)))))))
427 (defmethod copy [Reader File] [^Reader input ^File output]
428 (with-open [out (FileOutputStream. output)]
429 (copy input out)))
431 (defmethod copy [File OutputStream] [^File input ^OutputStream output]
432 (with-open [in (FileInputStream. input)]
433 (copy in output)))
435 (defmethod copy [File Writer] [^File input ^Writer output]
436 (with-open [in (FileInputStream. input)]
437 (copy in output)))
439 (defmethod copy [File File] [^File input ^File output]
440 (with-open [in (FileInputStream. input)
441 out (FileOutputStream. output)]
442 (copy in out)))
444 (defmethod copy [String OutputStream] [^String input ^OutputStream output]
445 (copy (StringReader. input) output))
447 (defmethod copy [String Writer] [^String input ^Writer output]
448 (copy (StringReader. input) output))
450 (defmethod copy [String File] [^String input ^File output]
451 (copy (StringReader. input) output))
453 (defmethod copy [*char-array-type* OutputStream] [input ^OutputStream output]
454 (copy (CharArrayReader. input) output))
456 (defmethod copy [*char-array-type* Writer] [input ^Writer output]
457 (copy (CharArrayReader. input) output))
459 (defmethod copy [*char-array-type* File] [input ^File output]
460 (copy (CharArrayReader. input) output))
462 (defmethod copy [*byte-array-type* OutputStream] [^"[B" input ^OutputStream output]
463 (copy (ByteArrayInputStream. input) output))
465 (defmethod copy [*byte-array-type* Writer] [^"[B" input ^Writer output]
466 (copy (ByteArrayInputStream. input) output))
468 (defmethod copy [*byte-array-type* File] [^"[B" input ^Writer output]
469 (copy (ByteArrayInputStream. input) output))
471 (defn make-parents
472 "Creates all parent directories of file."
473 [^File file]
474 (.mkdirs (.getParentFile file)))
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)
483 (defmethod to-byte-array *byte-array-type* [x] x)
485 (defmethod to-byte-array String [^String x]
486 (.getBytes x *default-encoding*))
488 (defmethod to-byte-array File [^File x]
489 (with-open [input (FileInputStream. x)
490 buffer (ByteArrayOutputStream.)]
491 (copy input buffer)
492 (.toByteArray buffer)))
494 (defmethod to-byte-array InputStream [^InputStream x]
495 (let [buffer (ByteArrayOutputStream.)]
496 (copy x buffer)
497 (.toByteArray buffer)))
499 (defmethod to-byte-array Reader [^Reader x]
500 (.getBytes (slurp* x) *default-encoding*))
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)
508 (defmethod relative-path-string String [^String s]
509 (relative-path-string (File. s)))
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)))
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)
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)))
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)))))
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)))
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)
558 (defmethod as-url URL [x] x)
560 (defmethod as-url URI [^URI x] (.toURL x))
562 (defmethod as-url String [^String x] (URL. x))
564 (defmethod as-url File [^File x] (.toURL x))