Clojure勉強日記(その28 データ型(その2
前回はまって1回では終わりませんでしたが、続けます。
前回データ型を定義して動くことが確認できたので、
今回は実際の中身を実装してみる形になりますね。
まず、実際にCryptoFilterの中身を実装したコードが以下のようになりました。
- src/reader/crypto.clj
(ns reader.crypto (:use [reader.io]) (:require [clojure.java.io :as io]) (:import (java.security KeyStore KeyStore$SecretKeyEntry KeyStore$PasswordProtection) (javax.crypto KeyGenerator Cipher CipherOutputStream CipherInputStream) (java.io FileOutputStream FileInputStream))) (defprotocol Filter (init-filter [filter]) (filter-output-stream [filter]) (filter-input-stream [filter])) (defn filter-key [filter] (let [passwordCharArray (.toCharArray (.password filter))] (with-open [fis (FileInputStream. (.keystore filter))] (-> (doto (KeyStore/getInstance "JCEKS") (.load fis passwordCharArray)) (.getKey "filter-key" passwordCharArray))))) (deftype CryptoFilter [filename keystore password] Filter (init-filter [filter] (let [passwordCharArray (.toCharArray (.password filter)) key (.generateKey (KeyGenerator/getInstance "AES")) keystoreObj (doto (KeyStore/getInstance "JCEKS") (.load nil passwordCharArray) (.setEntry "filter-key" (KeyStore$SecretKeyEntry. key) (KeyStore$PasswordProtection. passwordCharArray)))] (with-open [fos (FileOutputStream. (.keystore filter))] (.store keystoreObj fos passwordCharArray)))) (filter-output-stream [filter] (let [cipher (doto (Cipher/getInstance "AES") (.init Cipher/ENCRYPT_MODE (filter-key filter)))] (CipherOutputStream. (io/output-stream (.filename filter)) cipher))) (filter-input-stream [filter] (let [cipher (doto (Cipher/getInstance "AES") (.init Cipher/DECRYPT_MODE (filter-key filter)))] (CipherInputStream. (io/input-stream (.filename filter)) cipher))) IOFactory (makeReader [filter] (makeReader (filter-input-stream filter))) (makeWriter [filter] (makeWriter (filter-output-stream filter))) )
で、IOFactory側のコードが以下の通り。
- src/reader/io.clj
(ns reader.io (:import (java.io File FileInputStream FileOutputStream InputStream InputStreamReader OutputStream OutputStreamWriter BufferedReader BufferedWriter) (java.net Socket URL))) (defprotocol IOFactory "汎用の読込み/書込みを定義したプロトコル" (makeReader [this] "Creates a BufferedReader.") (makeWriter [this] "Creates a BufferedWriter.")) (extend-protocol IOFactory InputStream (makeReader [src] (-> src InputStreamReader. BufferedReader.)) (makeWriter [dst] (throw (IllegalArgumentException. "Can’t open as an InputStream."))) OutputStream (makeReader [src] (throw (IllegalArgumentException. "Can’t open as an OutputStream."))) (makeWriter [dst] (-> dst OutputStreamWriter. BufferedWriter.))) (defn min-spit [dst content] (with-open [writer (makeWriter dst)] (.write writer (str content)))) (defn min-slurp [src] (let [sb (new StringBuilder)] (with-open [reader (makeReader src)] (loop [readChar (.read reader)] (if (neg? readChar) (str sb) (do (.append sb (char readChar)) (recur (.read reader))))))))
上記のコードを用意した上で、実際にIOFactoryのプロトコルを実装したCriptoFilterがmin-slurp/min-spitの引数として使えるかを確認します。
=> (load-file "src/reader/io.clj") #'reader.io/min-slurp => (load-file "src/reader/crypto.clj") nil => (import 'reader.crypto.CryptoFilter) reader.crypto.CryptoFilter => (def test-filter (CryptoFilter. "filter-file" "keystore" "toomanysecrets")) #'user/test-filter => (.init-filter test-filter) nil => (use 'reader.io) nil => (min-spit test-filter "This is a test of the CryptoFilter.") nil # min-slurp関数を用いて文字列が保存されたことを確認 => (min-slurp test-filter) "This is a test of the CryptoFilter." # 保存した文字列が取得できることを確認
と、こんなノリでIOFactoryを実装したデータ型を用いた処理が実行できることが確認できました。
後は元々のClojure関数であるslurp/spitの引数として与えることができればOKです。
その場合、「src/reader/crypto.clj」に以下のコードを追加します。
(extend CryptoFilter clojure.java.io/IOFactory (assoc clojure.java.io/default-streams-impl :make-reader (fn [x opts] (makeReader x)) :make-writer (fn [x opts] (makeWriter x)) :make-input-stream (fn [x opts] (filter-input-stream x)) :make-output-stream (fn [x opts] (filter-input-stream x))))
追加してから再度test-filterの初期化を行うと以下のようになりました。
=> (min-spit test-filter "This is a test of the CryptoFilter.") nil => (min-slurp test-filter) "This is a test of the CryptoFilter." => (slurp test-filter) "This is a test of the CryptoFilter." => (spit test-filter "This is a test of the CryptoVault using spit and slurp.") nil => (slurp test-filter) "This is a test of the CryptoVault using spit and slurp." => (min-slurp test-filter) "This is a test of the CryptoVault using spit and slurp."
min-spit/min-slurpとspit/slurpが同様に動作しているのがわかります。
かつ、spit/slurpの枠組みにはめるのにCryptoFilter自体には修正を行っていません。
extend 節によって後付けでインタフェースを追加することができています。
・・・と、とりあえずデータ型については終了です。
本も後半戦になるにつれて一つ一つが重くなってきますが・・・まぁ、地道に行きましょう。