-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathbuilder.clj
More file actions
201 lines (186 loc) · 9.4 KB
/
builder.clj
File metadata and controls
201 lines (186 loc) · 9.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
;; Copyright (c) Sean Corfield. All rights reserved.
;; The use and distribution terms for this software are covered by the
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this
;; distribution. By using this software in any fashion, you are agreeing to
;; be bound by the terms of this license. You must not remove this notice,
;; or any other, from this software.
(ns
^{:author "Sean Corfield",
:doc "A variant of clojure.java.data/to-java that uses a Builder class
to build the requested class from a hash map of properties."}
clojure.java.data.builder
(:require [clojure.java.data :as j]))
(set! *warn-on-reflection* true)
(defn- get-builder-class [^Class clazz]
(try
(resolve (symbol (str (.getName clazz) "$Builder")))
(catch Throwable _)))
(defn- get-builder ^java.lang.reflect.Method [^Class clazz methods opts]
(let [build-name (:build-fn opts)
candidates
(filter (fn [^java.lang.reflect.Method m]
(and (= 0 (alength ^"[Ljava.lang.Class;" (.getParameterTypes m)))
(= clazz (.getReturnType m))
(or (nil? build-name)
(= build-name (.getName m)))))
methods)]
(case (count candidates)
0 (throw (IllegalArgumentException.
(str "Cannot find builder method that returns "
(.getName clazz))))
1 (first candidates)
(let [builds (filter (fn [^java.lang.reflect.Method m]
(= "build" (.getName m)))
candidates)]
(case (count builds)
0 (throw (IllegalArgumentException.
(str "Cannot find 'build' method that returns "
(.getName clazz))))
(first builds))))))
(defn- find-setters [^Class builder methods props opts]
(let [candidates
(filter (fn [^java.lang.reflect.Method m]
(and (= 1 (alength ^"[Ljava.lang.Class;" (.getParameterTypes m)))
(= builder (.getReturnType m))
(or (not (re-find #"^set[A-Z]" (.getName m)))
(not (:ignore-setters? opts)))))
methods)]
(->> candidates
(reduce
(fn [setter-map ^java.lang.reflect.Method m]
(let [prop (keyword
(cond (re-find #"^set[A-Z]" (.getName m))
(let [^String n (subs (.getName m) 3)]
(str (Character/toLowerCase (.charAt n 0)) (subs n 1)))
(re-find #"^with[A-Z]" (.getName m))
(let [^String n (subs (.getName m) 4)]
(str (Character/toLowerCase (.charAt n 0)) (subs n 1)))
:else
(.getName m)))]
(if (contains? props prop)
(if (contains? setter-map prop)
(let [clazz1 (#'j/get-setter-type (second (get setter-map prop)))
clazz2 (#'j/get-setter-type m)
p-val (get props prop)]
(cond (and (instance? clazz1 p-val)
(not (instance? clazz2 p-val)))
setter-map ; existing setter is a better match:
(and (not (instance? clazz1 p-val))
(instance? clazz2 p-val))
;; this setter is a better match:
(assoc setter-map prop [(#'j/make-setter-fn m) m])
:else ; neither is an obviously better match:
(throw (IllegalArgumentException.
(str "Duplicate setter found for " prop
" in " (.getName builder) " class")))))
(assoc setter-map prop [(#'j/make-setter-fn m) m]))
;; if we are not trying to set this property, ignore the setter:
setter-map)))
{})
(reduce-kv
(fn [m k v]
(assoc m k (first v)))
{}))))
(defn- build-on [instance setters ^Class clazz props]
(reduce-kv (fn [builder k v]
(if-let [setter (get setters (keyword k))]
(apply setter [instance v])
(#'j/throw-log-or-ignore-missing-setter k clazz)))
instance
props))
(comment
;; given a class, see if it has a nested Builder class
;; otherwise we'll need to be told the builder class
;; and possibly how to create it
(get-builder-class java.util.Locale)
;; from the builder class, look for arity-0 methods then return
;; the original class -- if there's only one, use it, if there
;; are multiple and one is called "build", use it, else error
(get-builder java.util.Locale (.getMethods java.util.Locale$Builder) {})
;; setters on a builder will have single arguments and will
;; return the builder class, and will either be:
;; * B propertyName( T )
;; * B setPropertyName( T )
;; treat both as setters; thrown exception if they clash
;; (maybe an option to ignore setXyz( T ) methods?)
(find-setters java.util.Locale$Builder (.getMethods java.util.Locale$Builder) {} {})
;; general pattern will be to:
;; * get the builder class somehow
;; * get its public methods
;; * identify its builder method (or be told it)
;; * identity its setters by name
;; * construct the builder (or be given an instance?)
;; * reduce over the input hash map,
;; * -- if setter matches key,
;; * -- then invoke, use result (use j/to-java to build value here?)
;; * -- else either log, ignore, or throw (per j/*to-java-object-missing-setter*)
;; * invoke builder on result, return that
(let [clazz java.util.Locale
props {:language "fr"}
opts {}
^Class builder (get-builder-class clazz)]
(.invoke (get-builder clazz (.getMethods builder) opts)
(build-on (j/to-java builder ^clojure.lang.APersistentMap {})
(find-setters builder (.getMethods builder) props opts)
builder
props)
nil)))
(defn to-java
"Given a class and a hash map of properties, figure out the Builder class,
figure out the setters for the Builder, construct an instance of it and
produce an instance of the original class. A hash map of options may also
be provided.
Alternatively, given a class, a builder instance, a hash map of properties,
and a hash map of options, figure out the setters for the builder class,
and use the builder instance to produce an instance of the original class.
Finally, given a class, a builder class, a builder instance (possibly of a
different class), a hash map of properties, and a hash map of options,
figure out the setters for the builder class, and use the builder instance
to produce an instance of the original class.
The following options may be provided:
* :builder-class -- the class that should be used for the builder process;
by default we'll assume an inner class of clazz called 'Builder',
* :builder-props -- properties used to construct and initialize an instance
of the builder class; defaults to an empty hash map; may have
:clojure.java.data/constructor as metadata to provide constructor
arguments for the builder instance,
* :build-fn -- the name of the method in the Builder class to use to
complete the builder process and return the desired class;
by default we'll try to deduce it, preferring 'build' if we find
multiple candidates,
* :ignore-setters? -- a flag to indicate that methods on the builder
class that begin with 'set' should be ignored, which may be
necessary to avoid ambiguous methods that look like builder properties;
by default 'setFooBar` will be treated as a builder property 'fooBar'
if it accepts a single argument and returns a builder instance."
([clazz props] (to-java clazz props {}))
([^Class clazz props opts]
(if-let [builder (or (:builder-class opts) (get-builder-class clazz))]
(to-java clazz builder (j/to-java builder (get opts :builder-props {})) props opts)
(throw (IllegalArgumentException.
(str "Unable to deduce a builder class for " (.getName clazz))))))
([clazz instance props opts]
(let [builder (or (:builder-class opts) (class instance))]
(to-java clazz builder instance props opts)))
([^Class clazz ^Class builder instance props opts]
(.invoke (get-builder clazz (.getMethods builder) opts)
(build-on instance
(find-setters builder (.getMethods builder) props opts)
builder
props)
nil)))
(comment
(to-java java.util.Locale {:language "fr" :region "EG"}
;; these options are all defaults
{:builder-class java.util.Locale$Builder
:builder-props {}
:build-fn "build"})
(to-java java.util.Locale (java.util.Locale$Builder.) {:language "fr" :region "EG"}
;; these options are all defaults
{#_#_:builder-class java.util.Locale$Builder})
(to-java java.util.Locale (java.util.Locale$Builder.) {:language "fr" :region "EG"}
;; these options are all defaults
{:builder-class java.util.Locale$Builder
:builder-props {}
:build-fn "build"}))