夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

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 節によって後付けでインタフェースを追加することができています。

・・・と、とりあえずデータ型については終了です。
本も後半戦になるにつれて一つ一つが重くなってきますが・・・まぁ、地道に行きましょう。