よっつめ

Clojureでのファイルの配置とreplでの動かし方

『プログラミング Clojure 第2版』p.153 にこのような記述がある。

ここまでのすべてのコードを .clj ファイルにまとめておこう。
読者のプロジェクトディレクトリに src/examples/datatypes サブディレクトリを掘って、そこに vault.clj というファイルを作ろう。

『プログラミング Clojure 第2版』オーム社 平成25年4月25日 第2版第1刷

で、そのあとに、

(ns examples.cryptovault_complete ... )

で始まるコードが載っている。

そのコードのファイル名は、”code/src/examples/cryptovault_complete.clj” と
なっている。

なんか、全然違うやん(T_T)

で、自分なりに以下のようにファイルを配置してみた。

フォルダ構成

./examples
├── datatypes
│   └── vault.clj
└── protocols
     └── io.clj

で、”cryptovault_complete.clj” という名前で掲載されているコードを “vault.clj” という名前で datatypes ディレクトリに配置する。

また、名前空間も、examples.cryptovalut_complete から
examples.datatypes.valut に変更する。

examples/datatypes/vault.clj


;; データ型のメソッド実装は、deftypeの中に直接書くことができる
;;
(ns examples.datatypes.vault
  (:require [clojure.java.io :as io]
            [examples.protocols.io :as proto])             ;; <1>
  (:import (java.security KeyStore KeyStore$SecretKeyEntry
                          KeyStore$PasswordProtection)
           (javax.crypto KeyGenerator Cipher CipherOutputStream
                         CipherInputStream)
           (java.io FileInputStream FileOutputStream)))

;; プロトコルの定義
;; CryptoVaultというデータ型に実装するために
;; プロトコルでメソッドを定義する
(defprotocol Vault
  (init-vault [vault])
  (vault-output-stream [vault])
  (vault-input-stream [vault]))

(defn vault-key [vault]
  (let [password (.toCharArray (.password vault))]
    (with-open [fis (FileInputStream. (.keystore vault))]
      (-> (doto (KeyStore/getInstance "JCEKS")
            (.load fis password))
          (.getKey "vault-key" password)))))

;; データ型 CryptoVault を定義
(deftype CryptoVault [filename keystore password]
  ;; Vault プロトコルを実装
  Vault
  (init-vault [vault]
    (let [password (.toCharArray (.password vault))
          key (.generateKey (KeyGenerator/getInstance "AES"))
          keystore (doto (KeyStore/getInstance "JCEKS")
                     (.load nil password)
                     (.setEntry "vault-key"
                                (KeyStore$SecretKeyEntry. key)
                                (KeyStore$PasswordProtection. password)))]
      (with-open [fos (FileOutputStream. (.keystore vault))]
        (.store keystore fos password))))

  (vault-output-stream [vault]
    (let [cipher (doto (Cipher/getInstance "AES")
                   (.init Cipher/ENCRYPT_MODE (vault-key vault)))]
      (CipherOutputStream. (io/output-stream (.filename vault)) cipher)))

  (vault-input-stream [vault]
    (let [cipher (doto (Cipher/getInstance "AES")
                   (.init Cipher/DECRYPT_MODE (vault-key vault)))]
      (CipherInputStream. (io/input-stream (.filename vault)) cipher)))

  ;; IOFactory プロトコルを実装
  proto/IOFactory
  (make-reader [vault]
    (proto/make-reader (vault-input-stream vault)))
  (make-writer [vault]
    (proto/make-writer (vault-output-stream vault))))

;; CryptoVaultを拡張して、clojure.java.io/IOFactoryプロトコルも
;; 実装する。
(extend CryptoVault
  clojure.java.io/IOFactory
  (assoc clojure.java.io/default-streams-impl
         :make-input-stream (fn [x opts] (vault-input-stream x))
         :make-output-stream (fn [x opts] (vault-output-stream x))))

また、コード中、<1> の記載から、io.clj は、examples/protocols ディレクトリを参照していると思うので、p.148 のプロトコルについてのコードは、examples/protocols/io.clj に配置する。

examples/protocols/io.clj

(ns examples.protocols.io
  (:import (java.io File InputStream OutputStream
                    FileInputStream InputStreamReader BufferedReader
                    FileOutputStream OutputStreamWriter BufferedWriter)
           (java.net Socket URL)))

(defprotocol IOFactory
  "A protocol for things that can be read from and written to."
  (make-reader [this] "Creates a BufferedReader.")
  (make-writer [this] "Creates a BufferedWriter."))

;;-------------------------------------------------------------
;; extend-protocolマクロ -- 複数のデータ型に対するひとつのプロトコルを
;;                          一度に書ける。
;; (extend-protocol protocol & specs)
;; 引数 protocol -- プロトコルの名前
;;      & specs -- 複数の、型名とメソッド実装

(extend-protocol IOFactory
  InputStream
  (make-reader [src]
    (-> src InputStreamReader. BufferedReader.))
  (make-writer [dst]
    (throw (IllegalArgumentException.
            "Can't open as an InputStream.")))

  OutputStream
  (make-reader [src]
    (throw (IllegalArgumentException.
            "Can't open as an OutputStream.")))
  (make-writer [dst]
    (-> dst OutputStreamWriter. BufferedWriter.))

  String
  (make-reader [src]
    (make-reader (FileInputStream. src)))
  (make-writer [dst]
    (make-writer (FileOutputStream. dst)))

  File
  (make-reader [src]
    (make-reader (FileInputStream. src)))
  (make-writer [dst]
    (make-writer (FileOutputStream. dst)))

  Socket
  (make-reader [src]
    (make-reader (.getInputStream src)))
  (make-writer [dst]
    (make-writer (.getOutputStream dst)))

  URL
  (make-reader [src]
    (make-reader
     (if (= "file" (.getProtocol src))
       (-> src .getPath FileInputStream.)
       (.openStream src))))
  (make-writer [dst]
    (make-writer
     (if (= "file" (.getProtocol dst))
       (-> dst .getPath FileInputStream.)
       (throw (IllegalArgumentException.
               "Can't write to non-file URL"))))))
;;--------------------------------------------------

;; make-readerを使った gulp に書き換える
(defn gulp [src]
  (let [sb (StringBuilder.)] 
    (with-open [reader (make-reader src)]
      (loop [c (.read reader)]
        (if (neg? c)
          (str sb) 
          (do
            (.append sb (char c))
            (recur (.read reader))))))))


;; expectorate を make-writer を使ったものに書き換える
(defn expectorate [dst content]
  (with-open [writer (make-writer dst)]
    (.write writer (str content))))

これを repl で動かしてみる。

src ディレクトリに examples ディレクトリ以下があるとして、 src/ にて repl を起動する。

src$ lein reple

まず、examples.datatypes.vaultの名前空間にアクセスできないと、

Unable to resolve symbol: ->CryptoVault in this context

と言われてしまう。

user=> (refer 'examples.datatypes.vault)
nil

user=> (def vault (->CryptoVault "vault-file" "keystore" "toomanysecrets"))
#'user/vault

user=> (spit vault "This is a test of the CryptoVault using spit and slurp")
nil

user=> (slurp vault)
"This is a test of the CryptoVault using spit and slurp"

gulp や expectorate のコマンドを起動できるように、examples.protocols.io名前空間を参照できるようにする。

user=> (refer 'examples.protocols.io)
nil

user=> (expectorate vault "This is a test of the CryptoVault")
nil

user=> (gulp vault)
"This is a test of the CryptoVault"

このやり方で合っているのか、他に適切なやり方があるのか、わからないけれど、とにかく動いた。

プログラミング Clojure 第2版

Stuart Halloway / Aaron Bedra 著
川合史朗 訳
平成25年4月25日 第2版第1刷 発行
オーム社