Reputation: 25269
Is there a way to easily generate java beans given a vector in clojure? For example given a vector like this:
[
String :key1
Integer :key2
]
I'd like it to generate code like this:
public class NotSureWhatTheTypeWouldBeHere {
private String key1;
private Integer key2;
public NotSureWhatTheTypeWouldBeHere() {}
public NotSureWhatTheTypeWouldBeHere(String key1, Integer key2) {
this.key1 = key1;
this.key2 = key2;
}
public void setKey1(String key1) {
this.key1 = key1;
}
public String getKey1() {
return this.key1;
}
public void setKey2(Integer key2) {
this.key2 = key2;
}
public String getKey2() {
return this.key2;
}
// and equals,hashCode, toString, etc.
}
For context, I'd like to write an app that is written in java but calls into a library written in clojure. So that means the return values should be java beans (I know they don't have to be, but I'd like them to be). One way would be to define the model in java and then use clojure's normal java interop to populate the model in the clojure code, but I like the idea of a concise clojure vector(or map) expanding out to a (verbose) java bean.
Upvotes: 8
Views: 1068
Reputation: 8591
Far from perfect and it probably has a lot of unforeseen issues when you try to use it, but I think you can start with something like:
(ns genbean)
(defn -init []
[[] (atom {})])
(defn -toString
[this]
(str @(.state this)))
(defn -equals
[this other]
(= @(.state this) @(.state other)))
(defn -hashCode
[this]
(hash @(.state this)))
(defn set-field
[this key value]
(swap! (.state this) into {key value}))
(defn get-field
[this key]
(@(.state this) key))
(defn gen-method-defs [fields]
(mapcat (fn [[name type]] [[(str "set" name) [type] 'void]
[(str "get" name) [] type]]) fields))
(defn def-access-methods [fields]
(mapcat (fn [field] [`(defgetter ~field) `(defsetter ~field)]) fields))
(defmacro defsetter [field]
`(defn ~(symbol (str "-set" field)) [this# value#]
(set-field this# ~(keyword field) value#)))
(defmacro defgetter [field]
`(defn ~(symbol (str "-get" field))
[this#]
(get-field this# ~(keyword field))))
(defmacro defbean [bean fields]
`(do
(gen-class
:main false
:state ~'state
:init ~'init
:name ~bean
:methods ~(gen-method-defs fields))
~@(def-access-methods (keys fields))
))
(defbean com.test.Foo {Bar Integer Baz String What int})
Using it from the java side:
Foo f = new Foo();
f.setBaz("hithere");
f.setBar(12);
System.out.println("f = " + f);
Foo f2 = new Foo();
System.out.println("f2.equals(f) = " + f2.equals(f));
f2.setBaz("hithere");
f2.setBar(12);
System.out.println("f2.equals(f) = " + f2.equals(f));
System.out.println("(f2.hashCode() == f.hashCode()) = " + (f2.hashCode() == f.hashCode()));
Produces:
f = {:Baz "hithere", :Bar 12}
f2.equals(f) = false
f2.equals(f) = true
(f2.hashCode() == f.hashCode()) = true
Note that you will need to compile the geanbean namespace. The implementation uses an atom to store all the properties, so be sure that you understand the trade-offs.
Also, when working in Clojure, you probably don't want to work with the javabeans but you can create a couple of methods to get and set the atom holding the state.
Upvotes: 2
Reputation: 46548
I guess it should be possible, but I'm not sure you fully understand keys in Clojure (it may be just that I'm misreading your example code).
Keys such as :name
are of type clojure.lang.Keyword
, not String
or Integer
etc (also you don't normally declare types in Clojure). They are often used in maps (which use the {} syntax) to retrieve values, for example the following code retrieves the value associated with :key2
from the map {:key1 "hello", :key2 4}
.
(get {:key1 "hello", :key2 4} :key2)
4
I'm not sure if your example is trying to say you have :key1
associated with a String
value and :key2
associated with a Integer
value or if you think :key1
is of type String
. If the former, probably want to use a map instead of a vector.
I'm afraid I don't think I know enough about Java beans or your use case in particular to help much further.
Upvotes: 0
Reputation: 13514
I don't think your Java code would play nicely with auto generated Java bean compliant classes. You need to have at least an interface on the Java side to make any sense of what's Clojure is going to return. Without that, you will have to revert to:
Object result = callClojureLib(params);
Then, no matter if the actual result implements the Java bean contract, your Java code will have to do all sorts of reflection wizardry to be able to even call a setter, as you're missing the class specification.
Another approach to the problem would be to use the java.util.Map
interface as the contract between Java and Clojure worlds. This way, you could just use plain Clojure maps as transfer objects, as they are assignable to java.util.Map
:
user=> (isa? (class {}) java.util.Map)
true
Upvotes: 4