diff 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 diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/clojure/contrib/duck_streams.clj	Sat Aug 21 06:25:44 2010 -0400
     1.3 @@ -0,0 +1,418 @@
     1.4 +;;; duck_streams.clj -- duck-typed I/O streams for Clojure
     1.5 +
     1.6 +;; by Stuart Sierra, http://stuartsierra.com/
     1.7 +;; May 13, 2009
     1.8 +
     1.9 +;; Copyright (c) Stuart Sierra, 2009. All rights reserved.  The use
    1.10 +;; and distribution terms for this software are covered by the Eclipse
    1.11 +;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
    1.12 +;; which can be found in the file epl-v10.html at the root of this
    1.13 +;; distribution.  By using this software in any fashion, you are
    1.14 +;; agreeing to be bound by the terms of this license.  You must not
    1.15 +;; remove this notice, or any other, from this software.
    1.16 +
    1.17 +
    1.18 +;; This file defines "duck-typed" I/O utility functions for Clojure.
    1.19 +;; The 'reader' and 'writer' functions will open and return an
    1.20 +;; instance of java.io.BufferedReader and java.io.PrintWriter,
    1.21 +;; respectively, for a variety of argument types -- filenames as
    1.22 +;; strings, URLs, java.io.File's, etc.  'reader' even works on http
    1.23 +;; URLs.
    1.24 +;;
    1.25 +;; Note: this is not really "duck typing" as implemented in languages
    1.26 +;; like Ruby.  A better name would have been "do-what-I-mean-streams"
    1.27 +;; or "just-give-me-a-stream", but ducks are funnier.
    1.28 +
    1.29 +
    1.30 +;; CHANGE LOG
    1.31 +;;
    1.32 +;; July 23, 2010: DEPRECATED in 1.2. Use clojure.java.io instead.
    1.33 +;;
    1.34 +;; May 13, 2009: added functions to open writers for appending
    1.35 +;;
    1.36 +;; May 3, 2009: renamed file to file-str, for compatibility with
    1.37 +;; clojure.contrib.java-utils.  reader/writer no longer use this
    1.38 +;; function.
    1.39 +;;
    1.40 +;; February 16, 2009: (lazy branch) fixed read-lines to work with lazy
    1.41 +;; Clojure.
    1.42 +;;
    1.43 +;; January 10, 2009: added *default-encoding*, so streams are always
    1.44 +;; opened as UTF-8.
    1.45 +;;
    1.46 +;; December 19, 2008: rewrote reader and writer as multimethods; added
    1.47 +;; slurp*, file, and read-lines
    1.48 +;;
    1.49 +;; April 8, 2008: first version
    1.50 +
    1.51 +(ns 
    1.52 +   ^{:author "Stuart Sierra",
    1.53 +     :deprecated "1.2"
    1.54 +     :doc "This file defines \"duck-typed\" I/O utility functions for Clojure.
    1.55 +           The 'reader' and 'writer' functions will open and return an
    1.56 +           instance of java.io.BufferedReader and java.io.PrintWriter,
    1.57 +           respectively, for a variety of argument types -- filenames as
    1.58 +           strings, URLs, java.io.File's, etc.  'reader' even works on http
    1.59 +           URLs.
    1.60 +
    1.61 +           Note: this is not really \"duck typing\" as implemented in languages
    1.62 +           like Ruby.  A better name would have been \"do-what-I-mean-streams\"
    1.63 +           or \"just-give-me-a-stream\", but ducks are funnier."} 
    1.64 +  clojure.contrib.duck-streams
    1.65 +  (:refer-clojure :exclude (spit))
    1.66 +  (:import 
    1.67 +   (java.io Reader InputStream InputStreamReader PushbackReader
    1.68 +            BufferedReader File PrintWriter OutputStream
    1.69 +            OutputStreamWriter BufferedWriter Writer
    1.70 +            FileInputStream FileOutputStream ByteArrayOutputStream
    1.71 +            StringReader ByteArrayInputStream)
    1.72 +   (java.net URI URL MalformedURLException Socket)))
    1.73 +
    1.74 +
    1.75 +(def
    1.76 + ^{:doc "Name of the default encoding to use when reading & writing.
    1.77 +  Default is UTF-8."
    1.78 +    :tag "java.lang.String"}
    1.79 + *default-encoding* "UTF-8")
    1.80 +
    1.81 +(def
    1.82 + ^{:doc "Size, in bytes or characters, of the buffer used when
    1.83 +  copying streams."}
    1.84 + *buffer-size* 1024)
    1.85 +
    1.86 +(def
    1.87 + ^{:doc "Type object for a Java primitive byte array."}
    1.88 + *byte-array-type* (class (make-array Byte/TYPE 0)))
    1.89 +
    1.90 +
    1.91 +(defn ^File file-str
    1.92 +  "Concatenates args as strings and returns a java.io.File.  Replaces
    1.93 +  all / and \\ with File/separatorChar.  Replaces ~ at the start of
    1.94 +  the path with the user.home system property."
    1.95 +  [& args]
    1.96 +  (let [^String s (apply str args)
    1.97 +        s (.replaceAll (re-matcher #"[/\\]" s) File/separator)
    1.98 +        s (if (.startsWith s "~")
    1.99 +            (str (System/getProperty "user.home")
   1.100 +                 File/separator (subs s 1))
   1.101 +            s)]
   1.102 +    (File. s)))
   1.103 +
   1.104 +
   1.105 +(defmulti ^{:tag BufferedReader
   1.106 +             :doc "Attempts to coerce its argument into an open
   1.107 +  java.io.BufferedReader.  Argument may be an instance of Reader,
   1.108 +  BufferedReader, InputStream, File, URI, URL, Socket, or String.
   1.109 +
   1.110 +  If argument is a String, it tries to resolve it first as a URI, then
   1.111 +  as a local file name.  URIs with a 'file' protocol are converted to
   1.112 +  local file names.  Uses *default-encoding* as the text encoding.
   1.113 +
   1.114 +  Should be used inside with-open to ensure the Reader is properly
   1.115 +  closed."
   1.116 +             :arglists '([x])}
   1.117 +  reader class)
   1.118 +
   1.119 +(defmethod reader Reader [x]
   1.120 +  (BufferedReader. x))
   1.121 +
   1.122 +(defmethod reader InputStream [^InputStream x]
   1.123 +  (BufferedReader. (InputStreamReader. x *default-encoding*)))
   1.124 +
   1.125 +(defmethod reader File [^File x]
   1.126 +  (reader (FileInputStream. x)))
   1.127 +
   1.128 +(defmethod reader URL [^URL x]
   1.129 +  (reader (if (= "file" (.getProtocol x))
   1.130 +            (FileInputStream. (.getPath x))
   1.131 +            (.openStream x))))
   1.132 +
   1.133 +(defmethod reader URI [^URI x]
   1.134 +  (reader (.toURL x)))
   1.135 +
   1.136 +(defmethod reader String [^String x]
   1.137 +  (try (let [url (URL. x)]
   1.138 +         (reader url))
   1.139 +       (catch MalformedURLException e
   1.140 +         (reader (File. x)))))
   1.141 +
   1.142 +(defmethod reader Socket [^Socket x]
   1.143 +  (reader (.getInputStream x)))
   1.144 +
   1.145 +(defmethod reader :default [x]
   1.146 +  (throw (Exception. (str "Cannot open " (pr-str x) " as a reader."))))
   1.147 +
   1.148 +
   1.149 +(def
   1.150 + ^{:doc "If true, writer and spit will open files in append mode.
   1.151 + Defaults to false.  Use append-writer or append-spit."
   1.152 +    :tag "java.lang.Boolean"}
   1.153 + *append-to-writer* false)
   1.154 +
   1.155 +
   1.156 +(defmulti ^{:tag PrintWriter
   1.157 +             :doc "Attempts to coerce its argument into an open java.io.PrintWriter
   1.158 +  wrapped around a java.io.BufferedWriter.  Argument may be an
   1.159 +  instance of Writer, PrintWriter, BufferedWriter, OutputStream, File,
   1.160 +  URI, URL, Socket, or String.
   1.161 +
   1.162 +  If argument is a String, it tries to resolve it first as a URI, then
   1.163 +  as a local file name.  URIs with a 'file' protocol are converted to
   1.164 +  local file names.
   1.165 +
   1.166 +  Should be used inside with-open to ensure the Writer is properly
   1.167 +  closed."
   1.168 +             :arglists '([x])}
   1.169 +  writer class)
   1.170 +
   1.171 +(defn- assert-not-appending []
   1.172 +  (when *append-to-writer*
   1.173 +    (throw (Exception. "Cannot change an open stream to append mode."))))
   1.174 +
   1.175 +(defmethod writer PrintWriter [x]
   1.176 +  (assert-not-appending)
   1.177 +  x)
   1.178 +
   1.179 +(defmethod writer BufferedWriter [^BufferedWriter x]
   1.180 +  (assert-not-appending)
   1.181 +  (PrintWriter. x))
   1.182 +
   1.183 +(defmethod writer Writer [x]
   1.184 +  (assert-not-appending)
   1.185 +  ;; Writer includes sub-classes such as FileWriter
   1.186 +  (PrintWriter. (BufferedWriter. x)))   
   1.187 +
   1.188 +(defmethod writer OutputStream [^OutputStream x]
   1.189 +  (assert-not-appending)
   1.190 +  (PrintWriter.
   1.191 +   (BufferedWriter.
   1.192 +    (OutputStreamWriter. x *default-encoding*))))
   1.193 +
   1.194 +(defmethod writer File [^File x]
   1.195 +  (let [stream (FileOutputStream. x *append-to-writer*)]
   1.196 +    (binding [*append-to-writer* false]
   1.197 +      (writer stream))))
   1.198 +
   1.199 +(defmethod writer URL [^URL x]
   1.200 +  (if (= "file" (.getProtocol x))
   1.201 +    (writer (File. (.getPath x)))
   1.202 +    (throw (Exception. (str "Cannot write to non-file URL <" x ">")))))
   1.203 +
   1.204 +(defmethod writer URI [^URI x]
   1.205 +  (writer (.toURL x)))
   1.206 +
   1.207 +(defmethod writer String [^String x]
   1.208 +  (try (let [url (URL. x)]
   1.209 +         (writer url))
   1.210 +       (catch MalformedURLException err
   1.211 +         (writer (File. x)))))
   1.212 +
   1.213 +(defmethod writer Socket [^Socket x]
   1.214 +  (writer (.getOutputStream x)))
   1.215 +
   1.216 +(defmethod writer :default [x]
   1.217 +  (throw (Exception. (str "Cannot open <" (pr-str x) "> as a writer."))))
   1.218 +
   1.219 +
   1.220 +(defn append-writer
   1.221 +  "Like writer but opens file for appending.  Does not work on streams
   1.222 +  that are already open."
   1.223 +  [x]
   1.224 +  (binding [*append-to-writer* true]
   1.225 +    (writer x)))
   1.226 +
   1.227 +
   1.228 +(defn write-lines
   1.229 +  "Writes lines (a seq) to f, separated by newlines.  f is opened with
   1.230 +  writer, and automatically closed at the end of the sequence."
   1.231 +  [f lines]
   1.232 +  (with-open [^PrintWriter writer (writer f)]
   1.233 +    (loop [lines lines]
   1.234 +      (when-let [line (first lines)]
   1.235 +        (.write writer (str line))
   1.236 +        (.println writer)
   1.237 +        (recur (rest lines))))))
   1.238 +
   1.239 +(defn read-lines
   1.240 +  "Like clojure.core/line-seq but opens f with reader.  Automatically
   1.241 +  closes the reader AFTER YOU CONSUME THE ENTIRE SEQUENCE."
   1.242 +  [f]
   1.243 +  (let [read-line (fn this [^BufferedReader rdr]
   1.244 +                    (lazy-seq
   1.245 +                     (if-let [line (.readLine rdr)]
   1.246 +                       (cons line (this rdr))
   1.247 +                       (.close rdr))))]
   1.248 +    (read-line (reader f))))
   1.249 +
   1.250 +(defn ^String slurp*
   1.251 +  "Like clojure.core/slurp but opens f with reader."
   1.252 +  [f]
   1.253 +  (with-open [^BufferedReader r (reader f)]
   1.254 +      (let [sb (StringBuilder.)]
   1.255 +        (loop [c (.read r)]
   1.256 +          (if (neg? c)
   1.257 +            (str sb)
   1.258 +            (do (.append sb (char c))
   1.259 +                (recur (.read r))))))))
   1.260 +
   1.261 +(defn spit
   1.262 +  "Opposite of slurp.  Opens f with writer, writes content, then
   1.263 +  closes f."
   1.264 +  [f content]
   1.265 +  (with-open [^PrintWriter w (writer f)]
   1.266 +      (.print w content)))
   1.267 +
   1.268 +(defn append-spit
   1.269 +  "Like spit but appends to file."
   1.270 +  [f content]
   1.271 +  (with-open [^PrintWriter w (append-writer f)]
   1.272 +    (.print w content)))
   1.273 +
   1.274 +(defn pwd
   1.275 +  "Returns current working directory as a String.  (Like UNIX 'pwd'.)
   1.276 +  Note: In Java, you cannot change the current working directory."
   1.277 +  []
   1.278 +  (System/getProperty "user.dir"))
   1.279 +
   1.280 +
   1.281 +
   1.282 +(defmacro with-out-writer
   1.283 +  "Opens a writer on f, binds it to *out*, and evalutes body.
   1.284 +  Anything printed within body will be written to f."
   1.285 +  [f & body]
   1.286 +  `(with-open [stream# (writer ~f)]
   1.287 +     (binding [*out* stream#]
   1.288 +       ~@body)))
   1.289 +
   1.290 +(defmacro with-out-append-writer
   1.291 +  "Like with-out-writer but appends to file."
   1.292 +  [f & body]
   1.293 +  `(with-open [stream# (append-writer ~f)]
   1.294 +     (binding [*out* stream#]
   1.295 +       ~@body)))
   1.296 +
   1.297 +(defmacro with-in-reader
   1.298 +  "Opens a PushbackReader on f, binds it to *in*, and evaluates body."
   1.299 +  [f & body]
   1.300 +  `(with-open [stream# (PushbackReader. (reader ~f))]
   1.301 +     (binding [*in* stream#]
   1.302 +       ~@body)))
   1.303 +
   1.304 +(defmulti
   1.305 +  ^{:doc "Copies input to output.  Returns nil.
   1.306 +  Input may be an InputStream, Reader, File, byte[], or String.
   1.307 +  Output may be an OutputStream, Writer, or File.
   1.308 +
   1.309 +  Does not close any streams except those it opens itself 
   1.310 +  (on a File).
   1.311 +
   1.312 +  Writing a File fails if the parent directory does not exist."
   1.313 +     :arglists '([input output])}
   1.314 +  copy
   1.315 +  (fn [input output] [(type input) (type output)]))
   1.316 +
   1.317 +(defmethod copy [InputStream OutputStream] [^InputStream input ^OutputStream output]
   1.318 +  (let [buffer (make-array Byte/TYPE *buffer-size*)]
   1.319 +    (loop []
   1.320 +      (let [size (.read input buffer)]
   1.321 +        (when (pos? size)
   1.322 +          (do (.write output buffer 0 size)
   1.323 +              (recur)))))))
   1.324 +
   1.325 +(defmethod copy [InputStream Writer] [^InputStream input ^Writer output]
   1.326 +  (let [^"[B" buffer (make-array Byte/TYPE *buffer-size*)]
   1.327 +    (loop []
   1.328 +      (let [size (.read input buffer)]
   1.329 +        (when (pos? size)
   1.330 +          (let [chars (.toCharArray (String. buffer 0 size *default-encoding*))]
   1.331 +            (do (.write output chars)
   1.332 +                (recur))))))))
   1.333 +
   1.334 +(defmethod copy [InputStream File] [^InputStream input ^File output]
   1.335 +  (with-open [out (FileOutputStream. output)]
   1.336 +    (copy input out)))
   1.337 +
   1.338 +(defmethod copy [Reader OutputStream] [^Reader input ^OutputStream output]
   1.339 +  (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)]
   1.340 +    (loop []
   1.341 +      (let [size (.read input buffer)]
   1.342 +        (when (pos? size)
   1.343 +          (let [bytes (.getBytes (String. buffer 0 size) *default-encoding*)]
   1.344 +            (do (.write output bytes)
   1.345 +                (recur))))))))
   1.346 +
   1.347 +(defmethod copy [Reader Writer] [^Reader input ^Writer output]
   1.348 +  (let [^"[C" buffer (make-array Character/TYPE *buffer-size*)]
   1.349 +    (loop []
   1.350 +      (let [size (.read input buffer)]
   1.351 +        (when (pos? size)
   1.352 +          (do (.write output buffer 0 size)
   1.353 +              (recur)))))))
   1.354 +
   1.355 +(defmethod copy [Reader File] [^Reader input ^File output]
   1.356 +  (with-open [out (FileOutputStream. output)]
   1.357 +    (copy input out)))
   1.358 +
   1.359 +(defmethod copy [File OutputStream] [^File input ^OutputStream output]
   1.360 +  (with-open [in (FileInputStream. input)]
   1.361 +    (copy in output)))
   1.362 +
   1.363 +(defmethod copy [File Writer] [^File input ^Writer output]
   1.364 +  (with-open [in (FileInputStream. input)]
   1.365 +    (copy in output)))
   1.366 +
   1.367 +(defmethod copy [File File] [^File input ^File output]
   1.368 +  (with-open [in (FileInputStream. input)
   1.369 +              out (FileOutputStream. output)]
   1.370 +    (copy in out)))
   1.371 +
   1.372 +(defmethod copy [String OutputStream] [^String input ^OutputStream output]
   1.373 +  (copy (StringReader. input) output))
   1.374 +
   1.375 +(defmethod copy [String Writer] [^String input ^Writer output]
   1.376 +  (copy (StringReader. input) output))
   1.377 +
   1.378 +(defmethod copy [String File] [^String input ^File output]
   1.379 +  (copy (StringReader. input) output))
   1.380 +
   1.381 +(defmethod copy [*byte-array-type* OutputStream] [^"[B" input ^OutputStream output]
   1.382 +  (copy (ByteArrayInputStream. input) output))
   1.383 +
   1.384 +(defmethod copy [*byte-array-type* Writer] [^"[B" input ^Writer output]
   1.385 +  (copy (ByteArrayInputStream. input) output))
   1.386 +
   1.387 +(defmethod copy [*byte-array-type* File] [^"[B" input ^Writer output]
   1.388 +  (copy (ByteArrayInputStream. input) output))
   1.389 +
   1.390 +
   1.391 +(defn make-parents
   1.392 +  "Creates all parent directories of file."
   1.393 +  [^File file]
   1.394 +  (.mkdirs (.getParentFile file)))
   1.395 +
   1.396 +(defmulti
   1.397 +  ^{:doc "Converts argument into a Java byte array.  Argument may be
   1.398 +  a String, File, InputStream, or Reader.  If the argument is already
   1.399 +  a byte array, returns it."
   1.400 +    :arglists '([arg])}
   1.401 +  to-byte-array type)
   1.402 +
   1.403 +(defmethod to-byte-array *byte-array-type* [x] x)
   1.404 +
   1.405 +(defmethod to-byte-array String [^String x]
   1.406 +  (.getBytes x *default-encoding*))
   1.407 +
   1.408 +(defmethod to-byte-array File [^File x]
   1.409 +  (with-open [input (FileInputStream. x)
   1.410 +              buffer (ByteArrayOutputStream.)]
   1.411 +    (copy input buffer)
   1.412 +    (.toByteArray buffer)))
   1.413 +
   1.414 +(defmethod to-byte-array InputStream [^InputStream x]
   1.415 +  (let [buffer (ByteArrayOutputStream.)]
   1.416 +    (copy x buffer)
   1.417 +    (.toByteArray buffer)))
   1.418 +
   1.419 +(defmethod to-byte-array Reader [^Reader x]
   1.420 +  (.getBytes (slurp* x) *default-encoding*))
   1.421 +