beanの逆関数unbean

POJO(一般のJavaオブジェクト)をbeanとみなして、プロパティのマップを返してくれる関数 bean がある。

http://clojuredocs.org/clojure_core/clojure.core/bean

beanで作ったマップからオブジェクトに戻したい場合もあると思うので、beanの逆関数 unbean を作ってみた。

(defn bean-setter-code [^Class klass]
  (let [bean-info (java.beans.Introspector/getBeanInfo klass)
        pds (.getPropertyDescriptors bean-info)
        obj-sym (gensym "obj_")
        map-sym (gensym "map_")]
    `(fn [~map-sym]
       (let [~(with-meta obj-sym {:tag (.getName klass)}) (.newInstance ~klass)]
         ~@(for [^java.beans.PropertyDescriptor pd pds
                 :let [method (.getWriteMethod pd)]
                 :when method
                 :let [prop-key (keyword (.getName pd))
                       method-sym (symbol (.getName method))]]
             `(let [v# (get ~map-sym ~prop-key ::nothing)]
                (when-not (= v# ::nothing)
                  (. ~obj-sym ~method-sym v#))))
         ~obj-sym))))

(defn gen-bean-setter [klass]
  (eval (bean-setter-code klass)))

(def get-bean-setter (memoize gen-bean-setter))

(defn unbean [m]
  ((get-bean-setter (:class m)) m))

実行結果はこんな感じ。

user=> (def d (java.util.Date.))
#'user/d
user=> d
#inst "2012-07-28T13:16:02.273-00:00"
user=> (def b (bean d))
#'user/b
user=> b
{:seconds 2, :date 28, :class java.util.Date, :minutes 16, :hours 22, :year 112, :timezoneOffset -540, :month 6, :day 6, :time 1343481362273}
user=> (unbean b)
#inst "2012-07-28T13:16:02.273-00:00"

beanのクラス一つごとに、Fnが一つ定義されるので、たくさんのクラスを扱う場合はPermGenの容量に注意が必要。
(たいていはあんまり考える必要は無いので、unbeanのなかでは普通にmemoizeしている。)

マクロ内の型ヒント

ちなみに、マクロ内の型ヒントはクラスオブジェクトじゃダメで、クラス名のシンボルか文字列じゃないといけないらしい。
いつまでたってもReflection warningが消えてくれない理由がわからなくて、ちょっとハマった。