Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
DefaultVarExploder |
|
| 6.0;6 |
1 | /* | |
2 | * Copyright 2012, Ryan J. McDonough | |
3 | * | |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | * you may not use this file except in compliance with the License. | |
6 | * You may obtain a copy of the License at | |
7 | * | |
8 | * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | * | |
10 | * Unless required by applicable law or agreed to in writing, software | |
11 | * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | * See the License for the specific language governing permissions and | |
14 | * limitations under the License. | |
15 | */ | |
16 | package com.damnhandy.uri.template; | |
17 | ||
18 | import java.lang.reflect.Field; | |
19 | import java.lang.reflect.InvocationTargetException; | |
20 | import java.lang.reflect.Method; | |
21 | import java.util.*; | |
22 | ||
23 | ||
24 | /** | |
25 | * <p> | |
26 | * The {@link DefaultVarExploder} is a {@link VarExploder} implementation that takes in a Java object and | |
27 | * extracts the properties for use in a URI Template. Given the following URI expression: | |
28 | * </p> | |
29 | * <pre> | |
30 | * /mapper{?address*} | |
31 | * </pre> | |
32 | * <p> | |
33 | * And this Java object for an address: | |
34 | * </p> | |
35 | * <pre> | |
36 | * Address address = new Address(); | |
37 | * address.setState("CA"); | |
38 | * address.setCity("Newport Beach"); | |
39 | * String result = UriTemplate.fromTemplate("/mapper{?address*}").set("address", address).expand(); | |
40 | * </pre> | |
41 | * <p> | |
42 | * The expanded URI will be: | |
43 | * </p> | |
44 | * <pre> | |
45 | * /mapper?city=Newport%20Beach&state=CA | |
46 | * </pre> | |
47 | * <p> | |
48 | * <p> | |
49 | * The {@link DefaultVarExploder} breaks down the object properties as follows: | |
50 | * <ul> | |
51 | * <li>All properties that contain a non-null return value will be included</li> | |
52 | * <li>Getters or fields annotated with {@link UriTransient} will <b>NOT</b> included in the list</li> | |
53 | * <li>By default, the property name is used as the label in the URI. This can be overridden by | |
54 | * placing the {@link VarName} annotation on the field or getter method and specifying a name.</li> | |
55 | * <li>Field level annotation take priority of getter annotations</li> | |
56 | * </ul> | |
57 | * | |
58 | * @author <a href="ryan@damnhandy.com">Ryan J. McDonough</a> | |
59 | * @version $Revision: 1.1 $ | |
60 | * @see VarName | |
61 | * @see UriTransient | |
62 | * @see VarExploder | |
63 | * @since 1.0 | |
64 | */ | |
65 | public class DefaultVarExploder implements VarExploder | |
66 | { | |
67 | ||
68 | /** | |
69 | * | |
70 | */ | |
71 | private static final String GET_PREFIX = "get"; | |
72 | ||
73 | /** | |
74 | * | |
75 | */ | |
76 | private static final String IS_PREIX = "is"; | |
77 | ||
78 | ||
79 | /** | |
80 | * The original object. | |
81 | */ | |
82 | private Object source; | |
83 | ||
84 | /** | |
85 | * The objects properties that have been extracted to a {@link Map} | |
86 | */ | |
87 | 238 | private Map<String, Object> pairs = new TreeMap<String, Object>(); |
88 | ||
89 | /** | |
90 | * @param source the Object to explode | |
91 | */ | |
92 | public DefaultVarExploder(Object source) throws VarExploderException | |
93 | 238 | { |
94 | 238 | this.setSource(source); |
95 | 238 | } |
96 | ||
97 | /** | |
98 | * @return the name value pairs of the input | |
99 | */ | |
100 | @Override | |
101 | public Map<String, Object> getNameValuePairs() | |
102 | { | |
103 | 134 | return pairs; |
104 | } | |
105 | ||
106 | ||
107 | /** | |
108 | * | |
109 | * @param source | |
110 | * @throws VarExploderException | |
111 | */ | |
112 | void setSource(Object source) throws VarExploderException | |
113 | { | |
114 | 238 | this.source = source; |
115 | 238 | this.initValues(); |
116 | 238 | } |
117 | ||
118 | /** | |
119 | * Initializes the values from the object properties and constructs a | |
120 | * map from those values. | |
121 | * | |
122 | * @throws VarExploderException | |
123 | */ | |
124 | private void initValues() throws VarExploderException | |
125 | { | |
126 | ||
127 | 238 | Class<?> c = source.getClass(); |
128 | 238 | if (c.isAnnotation() || c.isArray() || c.isEnum() || c.isPrimitive()) |
129 | { | |
130 | 0 | throw new IllegalArgumentException("The value must an object"); |
131 | } | |
132 | ||
133 | 238 | if(source instanceof Map ) |
134 | { | |
135 | 72 | this.pairs = (Map<String,Object>) source; |
136 | 72 | return; |
137 | } | |
138 | 166 | Method[] methods = c.getMethods(); |
139 | 6616 | for (Method method : methods) |
140 | { | |
141 | 6450 | inspectGetters(method); |
142 | } | |
143 | 166 | scanFields(c); |
144 | 166 | } |
145 | ||
146 | /** | |
147 | * A lite version of the introspection logic performed by the BeanInfo introspector. | |
148 | * @param method | |
149 | */ | |
150 | private void inspectGetters(Method method) | |
151 | { | |
152 | 6450 | String methodName = method.getName(); |
153 | 6450 | int prefixLength = 0; |
154 | 6450 | if (methodName.startsWith(GET_PREFIX)) { |
155 | 426 | prefixLength = GET_PREFIX.length(); |
156 | } | |
157 | ||
158 | 6450 | if (methodName.startsWith(IS_PREIX)) { |
159 | 158 | prefixLength = IS_PREIX.length(); |
160 | } | |
161 | 6450 | if(prefixLength == 0) |
162 | { | |
163 | 5866 | return; |
164 | } | |
165 | ||
166 | 584 | String name = decapitalize(methodName.substring(prefixLength)); |
167 | 584 | if(!isValidProperty(name)) |
168 | { | |
169 | 142 | return; |
170 | } | |
171 | ||
172 | // Check that the return type is not null or void | |
173 | 442 | Class propertyType = method.getReturnType(); |
174 | ||
175 | 442 | if (propertyType == null || propertyType == void.class) |
176 | { | |
177 | 0 | return; |
178 | } | |
179 | ||
180 | // isXXX return boolean | |
181 | 442 | if (prefixLength == 2) |
182 | { | |
183 | 158 | if (!(propertyType == boolean.class)) |
184 | { | |
185 | 0 | return; |
186 | } | |
187 | } | |
188 | ||
189 | // validate parameter types | |
190 | 442 | Class[] paramTypes = method.getParameterTypes(); |
191 | 442 | if (paramTypes.length > 1 || |
192 | (paramTypes.length == 1 && paramTypes[0] != int.class)) | |
193 | { | |
194 | 0 | return; |
195 | } | |
196 | ||
197 | 442 | if (!method.isAnnotationPresent(UriTransient.class) && !"class".equals(name)) |
198 | { | |
199 | 276 | Object value = getValue(method); |
200 | ||
201 | 276 | if (method.isAnnotationPresent(VarName.class)) |
202 | { | |
203 | 0 | name = method.getAnnotation(VarName.class).value(); |
204 | } | |
205 | 276 | if (value != null) |
206 | { | |
207 | 262 | pairs.put(name, value); |
208 | } | |
209 | } | |
210 | 442 | } |
211 | ||
212 | private static boolean isValidProperty(String propertyName) { | |
213 | 584 | return (propertyName != null) && (propertyName.length() != 0); |
214 | } | |
215 | ||
216 | static String decapitalize(String name) | |
217 | { | |
218 | ||
219 | 584 | if (name == null) |
220 | 0 | return null; |
221 | // The rule for decapitalize is that: | |
222 | // If the first letter of the string is Upper Case, make it lower case | |
223 | // UNLESS the second letter of the string is also Upper Case, in which case no | |
224 | // changes are made. | |
225 | 584 | if (name.length() == 0 || (name.length() > 1 && Character.isUpperCase(name.charAt(1)))) { |
226 | 142 | return name; |
227 | } | |
228 | ||
229 | 442 | char[] chars = name.toCharArray(); |
230 | 442 | chars[0] = Character.toLowerCase(chars[0]); |
231 | 442 | return new String(chars); |
232 | } | |
233 | ||
234 | /** | |
235 | * Scans the fields on the class or super classes to look for | |
236 | * field-level annotations. | |
237 | * | |
238 | * @param c | |
239 | */ | |
240 | private void scanFields(Class<?> c) | |
241 | { | |
242 | 458 | if (!c.isInterface()) |
243 | { | |
244 | 458 | Field[] fields = c.getDeclaredFields(); |
245 | 1870 | for (Field field : fields) |
246 | { | |
247 | 1412 | String fieldName = field.getName(); |
248 | ||
249 | 1412 | if (pairs.containsKey(fieldName)) |
250 | { | |
251 | 120 | if (field.isAnnotationPresent(UriTransient.class)) |
252 | { | |
253 | 8 | pairs.remove(fieldName); |
254 | } | |
255 | 112 | else if (field.isAnnotationPresent(VarName.class)) |
256 | { | |
257 | 14 | String name = field.getAnnotation(VarName.class).value(); |
258 | 14 | pairs.put(name, pairs.get(fieldName)); |
259 | 14 | pairs.remove(fieldName); |
260 | } | |
261 | } | |
262 | } | |
263 | } | |
264 | /* | |
265 | * We still need to scan the fields of the super class if its | |
266 | * not Object to check for annotations. There might be a better | |
267 | * way to do this. | |
268 | */ | |
269 | 458 | if (!c.getSuperclass().equals(Object.class)) |
270 | { | |
271 | 292 | scanFields(c.getSuperclass()); |
272 | } | |
273 | 458 | } |
274 | ||
275 | /** | |
276 | * Return the value of the property. | |
277 | * | |
278 | * @param method | |
279 | * @return | |
280 | * @throws VarExploderException | |
281 | */ | |
282 | private Object getValue(Method method) throws VarExploderException | |
283 | { | |
284 | try | |
285 | { | |
286 | 276 | if (method == null) |
287 | { | |
288 | 0 | return null; |
289 | } | |
290 | 276 | return method.invoke(source); |
291 | } | |
292 | 0 | catch (IllegalArgumentException e) |
293 | { | |
294 | 0 | throw new VarExploderException(e); |
295 | } | |
296 | 0 | catch (IllegalAccessException e) |
297 | { | |
298 | 0 | throw new VarExploderException(e); |
299 | } | |
300 | 0 | catch (InvocationTargetException e) |
301 | { | |
302 | 0 | throw new VarExploderException(e); |
303 | } | |
304 | } | |
305 | ||
306 | @Override | |
307 | public Collection<Object> getValues() throws VarExploderException | |
308 | { | |
309 | 104 | return pairs.values(); |
310 | } | |
311 | ||
312 | } |