『プログラミング Clojure 第2版』p.153 にこのような記述がある。
ここまでのすべてのコードを .clj ファイルにまとめておこう。
『プログラミング Clojure 第2版』オーム社 平成25年4月25日 第2版第1刷
読者のプロジェクトディレクトリに src/examples/datatypes サブディレクトリを掘って、そこに vault.clj というファイルを作ろう。
で、そのあとに、
(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"
このやり方で合っているのか、他に適切なやり方があるのか、わからないけれど、とにかく動いた。