17

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"))

6 Answers 6

25

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}
Sign up to request clarification or add additional context in comments.

3 Comments

This looks promising, I will give it a try. I may have to do a recursive bean application.
Have a look at clojure.java.data. There is a function (from-java) that uses java.beans.Introspector recursively. Furthermore, you can refine the output of from-java by adding ...BeanInfo classes to a java library to control which get... methods are recognised, and to rename the keywords that are used in the clojure map.
Now you can also try bean-dip and similar libraries: github.com/uwcpdx/bean-dip (I'm the author of former but describe latter in the README)
5

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...

Comments

1

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.

1 Comment

I edited my example to show that I want to treat the parse result as a Clojure map. I have no problem making all the fields in the parse result final, if that helps any.
1

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"}

1 Comment

I don't see how to use this ... I have a Java class already, and want to use it as though it were a Clojure object .... are you suggesting that I convert my java object to a HashMap?
0

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}

Comments

-2
user=> (defn parser [text]
  "{ :items { \"itemid\" 55 }}");Mock
user=> (let [parse-tree (read-string (parser "Abracadabra"))]
((parse-tree :items) "itemid"))
55

3 Comments

Could you explain this? I don't understand how this solves my problem.
if your Parser output clojure map format string , use read-string, you can convert.
This has nothing to do with the question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.