Home

Awesome

gavagai

Easy to use conversion library between tree-like POJOs or anything else presenting a bean-like interface and Clojure data structures. It can be used as a reasonably fast, configurable and recursive replacement for clojure.core/bean. It is intended as a tool to easily build a bridge between Clojure and Java when writing wrapper libraries.

Motivation

Many Java APIs of perfectly good and usable libraries are based on the premises of returning data under the shape of trees of ad-hoc objects. Here in Clojureland, we have learned the benefits of using data under the form of uniform well-known structures, such as vectors and maps. They are optionally lazy, easier to reason about, fun to explore at the REPL, and so on and so forth... But in every ad-hoc mutable object from Javaland, there is a small core of data waiting to be freed. This is what gavagai endeavours to do, provide a simple, not too crazy and magical, declarative way to translate from Javaland to pure, immutable Clojureland data. It is also intended to be fast enough, and configurable enough to go wherever it needs to go to extract our precious data. It achieves these goals by using once-only reflection on the Java classes to create translation functions.

And if you wonder about the name gavagai, it is here to remind us that, though something is always lost in translation, we should still do our best to communicate with the people of our neighbouring lands...

Installation

gavagai is available as a Maven artifact from Clojars:

Clojars Project

Usage

You need first to register in a Translator a converter for every Java class you need to have translated. By default, a Translator is created for you, initiliazed and returned with the converters registered:

(ns my.nspace
  (:require [gavagai.core :as g]))

(def translator
  (g/register-converters
    {:exclude [:class]}
    [["java.util.Date" :add {:string str}]
     ["java.awt.Color" :only [:green #"r.d" :blue] :add {:string str} :lazy? false]
     ["java.awt.Button" :translate-arrays? true]
     ["java.awt.Button$AccessibleAWTButton" :exclude [:locale]]]))

The register-converters accepts a map as an optional first argument, which is the default options for every class registered. Individual options will be merged with the default options.

You can register a class by giving its name as a string, and add optional arguments to the declaration:

You can then call translate with the correct Translator on any registered object belonging to a registered class and itself and its members will be recursively translated. The translate function takes a map as an optional third argument, these params override the ones given in the converter.

(let [color (java.awt.Color. 10 10 10)]
      (g/translate translator color))
=> {:red 10, :blue 10, :green 10, :string "java.awt.Color[r=10,g=10,b=10]"}

;; You can also create a translating fn with the translator baked-in
;; (remenber Translators are immutable structures)
(def my-translate (partial g/translate translator))
(my-translate (java.awt.Color. 10 10 10))
=> {:red 10, :blue 10, :green 10, :string "java.awt.Color[r=10,g=10,b=10]"}

;; There is also a macro to avoid specifying the Translator:
(let [b (java.awt.Button. "test")]
      (g/with-translator translator
        (g/translate b {:max-depth 2})])
=> {:accessible-context
    {:accessible-role #<AccessibleRole push button>,
     :accessible-action
       ;; though registered, this does not get translated because it is 3 levels deep
     #<AccessibleAWTButton java.awt.Button$AccessibleAWTButton@1bfdfa36>,
     :background nil,
     :foreground nil,
       ;; snip...
     :focusable? true,
     :label "test",
     :font nil}}

The map keys are keywords obtained by removing the get or is prefix, hyphenizing the java method name, and adding a final ? if the method returns a boolean. You can check what keys gavagai will use for every eligible methods by using the inspect-class function. You can also use regexp patterns instead of keywords to select methods in the options.

(g/inspect-class java.util.Date)
=> {"getTimezoneOffset" :timezone-offset, "getClass" :class, "getTime"
:time, "getDate" :date, "getDay" :day, "getMinutes" :minutes,
"getSeconds" :seconds, "getMonth" :month, "getYear" :year, "getHours"
:hours}

(g/inspect-class String)
=> {"isEmpty" :empty?, "getBytes" :bytes, "getClass" :class}

There are also functions to inspect the fields returned by a converter, with all declared options taken into account:

(g/get-class-fields translator java.awt.Color)
=> #{:red :string :blue :green}

If you need to register a custom converter for a class, you can do it with add-converter. The converter is a plain Clojure function that takes 3 parameters, [translator object runtime-opts].

(let [color (java.awt.Color. 10 10 10)
      f (fn [_ ^java.aws.Color color _]
          (str "#" (apply str (drop 2 (Integer/toHexString (.getRGB color))))))
      new-translator (g/add-converter translator java.aws.Color f)
        ;; there is also a simpler syntax with register-converters
      same-translator (g/register-converters translator
                        [["java.aws.Color" :custom-converter f]])]
  (g/translate new-translator color)
=> "#0a0a0a"

To see a full-fledged exemple of gavagai use to build a wrapper around a very Java-centric API, you can check the code of clj-rome.

Related Info and Blog Posts

Performance and Caveats

The resulting maps are by default fully lazy (as core/bean). If you need to serialize or pass around the value, you should call translate with the lazy? set to false, to get a fully realized structure. Be careful about infinite loop in objects graph if you do this. You can set :omit-cyclic-ref? to true or specify a :max-depth when calling translate to guard against this.

License

Copyright © 2012, 2013 Nils Grunwald

Distributed under the Eclipse Public License, the same as Clojure.