Frage

I have a Java class that I'd like to use in Clojure. But, I want to use it as a Clojure map. What are the steps required to do so?

I've looked at the code for IPersistentMap -- should the Java class implement that? Or should there be some Clojure code which implements a protocol?

I know I could just write some mapping code, to explicitly convert the code from Java objects to maps, but that solution has a high effort/reward ratio. Also, I might encounter this same situation many more times.


Concrete example: I have a parser written in Java. I'd like to use that to parse some text, and then access the contents of the parsed data structure as though it were in Clojure maps:

(def parser (new MyParser))

(let [parse-tree (parser ... parse some text ...)]
  ((parse-tree :items) "itemid"))
War es hilfreich?

Lösung

The function bean came to mind:

Takes a Java object and returns a read-only implementation of the map abstraction based upon its JavaBean properties.

Example taken from the site:

user=> (import java.util.Date)
java.util.Date

user=> (def *now* (Date.))
#'user/*now*

user=> (bean *now*)
{:seconds 57, :date 13, :class java.util.Date,
 :minutes 55, :hours 17, :year 110, :timezoneOffset -330,
 :month 6, :day 2, :time 1279023957492}

Andere Tipps

Sure the (bean javaObject) (see bean ClojureDoc) works well, but it doesn't let you select the property you want and those you doesn't. It has impact when you input the resulting map into the json-str function, in that case you can get an error saying : "Don't know how to write JSON of ..."

And I find that annoying when I deal with NoSQL DB (mongoDB, neo4j) that accepts essentially JSON (like the underlying of neocons).

So what's my solution?

(defmacro get-map-from-object-props [object & props]
  ;->> will eval and reorder the next list starting from the end
  (->> (identity props) ;identity is here to return the 'props' seq
       ;map each property with their name as key and the java object invocation as the value
       ;the ~@ is here to unsplice the few properties
       (map (fn [prop] [(keyword (str prop)) `(.. ~object ~@(prop-symbol prop) )]))
       (into {})))

;getter is a simple function that transform a property name to its getter "name" -> "getName"
(defn prop-symbol [prop]
  (map symbol (map getter (clojure.string/split (str prop) #"\\."))))

And you can use it like that (yes the function takes care of a chain of property if any)

(get-map-from-object-props javaObject property1 property2 property3.property1)

Hope that will help someone...

Clojure keywords can look up stuff in anything that implements the required (read-only) parts of the java.lang.Map interface. The problem is probably going to be that you're not actually using clojure keywords as keys so that might not help you.

As for IPersistentMap; your parser presumably doesn't implement anything relevant to the that interface.

Personally, I'd write a straight up conversion function. Clojure uses a lot of those (seq, for instance) and after converting, you know you're dealing with a real persistent map and not something that only acts like it some of the time (so you can actually call seq, keys, vals etc on it).

Alternatively;

  • just implement clojure.lang.ILookup, and leave out everything else.
  • convert using some generated/reflection code if you want something more generic. See https://github.com/joodie/clj-java-fields for an example.

What about just using a java.util.HashMap with (interned) strings as keys, and doing the conversion in a few lines of Clojure ?:

(into {} (java.util.HashMap. {"foo" "bar" "baz" "quux"})) ?

{"foo" "bar" "baz" "quux"}

or with keywords:

(into {}
  (map
    (juxt
      #(keyword (key %))
      #(val %))
    (java.util.HashMap. {"foo" "bar" "baz" "quux"})))

{:baz "quux", :foo "bar"}

bean works fine, but it does not handle some Java objects very well.

(import java.awt.Insets)
(bean (Insets. 1 2 3 4))
=> {:class java.awt.Insets}

but there is a java solution to this java problem:

(import (com.fasterxml.jackson.databind ObjectMapper))
(import (java.util Map))
(into {} (.. (ObjectMapper.) (convertValue (Insets. 1 2 3 4) Map)))
=> {"top" 1, "left" 2, "bottom" 3, "right" 4}
user=> (defn parser [text]
  "{ :items { \"itemid\" 55 }}");Mock
user=> (let [parse-tree (read-string (parser "Abracadabra"))]
((parse-tree :items) "itemid"))
55
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top