-
Notifications
You must be signed in to change notification settings - Fork 218
/
BeanExtractor.java
324 lines (295 loc) · 12.9 KB
/
BeanExtractor.java
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
package org.jolokia.converter.json;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;
import javax.management.AttributeNotFoundException;
import org.jolokia.converter.object.StringToObjectConverter;
import org.jolokia.util.EscapeUtil;
import org.json.simple.JSONAware;
import org.json.simple.JSONObject;
/*
* Copyright 2009-2013 Roland Huss
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Extractor for plain Java objects.
*
* @author roland
* @since Apr 19, 2009
*/
public class BeanExtractor implements Extractor {
private static final Set<Class> FINAL_CLASSES = new HashSet<Class>(Arrays.asList(
String.class,
Number.class,
Byte.class,
Double.class,
Float.class,
Long.class,
Short.class,
Integer.class,
Boolean.class
));
private static final Set<String> IGNORE_METHODS = new HashSet<String>(Arrays.asList(
"getClass",
// Ommit internal stuff
"getStackTrace",
"getClassLoader"
));
private static final Class[] IGNORED_RETURN_TYPES = new Class[]{
OutputStream.class,
Writer.class
};
private static final String[] GETTER_PREFIX = new String[]{"get", "is", "has"};
/** {@inheritDoc} */
public Class getType() {
return Object.class;
}
/** {@inheritDoc} */
@SuppressWarnings("PMD.CompareObjectsWithEquals")
public Object extractObject(ObjectToJsonConverter pConverter, Object pValue,
Stack<String> pPathParts, boolean jsonify)
throws AttributeNotFoundException {
// Wrap fault handler if a wildcard path pattern is present
ValueFaultHandler faultHandler = pConverter.getValueFaultHandler();
String pathPart = pPathParts.isEmpty() ? null : pPathParts.pop();
if (pathPart != null) {
// Still some path elements available, so dive deeper
Object attributeValue = extractBeanPropertyValue(pValue, pathPart, faultHandler);
return pConverter.extractObject(attributeValue, pPathParts, jsonify);
} else {
if (jsonify) {
// We need the jsonfied value from here on.
return exctractJsonifiedValue(pConverter, pValue, pPathParts);
} else {
// No jsonification requested, hence we are returning the object itself
return pValue;
}
}
}
// Using standard set semantics
/** {@inheritDoc} */
public Object setObjectValue(StringToObjectConverter pConverter,Object pInner, String pAttribute, Object pValue)
throws IllegalAccessException, InvocationTargetException {
// Move this to plain object handler
String rest = new StringBuffer(pAttribute.substring(0,1).toUpperCase())
.append(pAttribute.substring(1)).toString();
String setter = new StringBuffer("set").append(rest).toString();
String getter = new StringBuffer("get").append(rest).toString();
Class clazz = pInner.getClass();
Method found = null;
for (Method method : clazz.getMethods()) {
if (method.getName().equals(setter)) {
found = method;
break;
}
}
if (found == null) {
throw new IllegalArgumentException(
"No Method " + setter + " known for object of type " + clazz.getName());
}
Class params[] = found.getParameterTypes();
if (params.length != 1) {
throw new IllegalArgumentException(
"Invalid parameter signature for " + setter + " known for object of type "
+ clazz.getName() + ". Setter must take exactly one parameter.");
}
Object oldValue;
try {
final Method getMethod = clazz.getMethod(getter);
AccessController.doPrivileged(new SetMethodAccessibleAction(getMethod));
oldValue = getMethod.invoke(pInner);
} catch (NoSuchMethodException exp) {
// Ignored, we simply dont return an old value
oldValue = null;
}
AccessController.doPrivileged(new SetMethodAccessibleAction(found));
found.invoke(pInner,pConverter.prepareValue(params[0].getName(), pValue));
return oldValue;
}
/** {@inheritDoc} */
public boolean canSetValue() {
return true;
}
// =====================================================================================================
private Object exctractJsonifiedValue(ObjectToJsonConverter pConverter, Object pValue, Stack<String> pPathParts)
throws AttributeNotFoundException {
if (pValue.getClass().isPrimitive() || FINAL_CLASSES.contains(pValue.getClass()) || pValue instanceof JSONAware) {
// No further diving, use these directly
return pValue;
} else {
// For the rest we build up a JSON map with the attributes as keys and the value are
List<String> attributes = extractBeanAttributes(pValue);
if (attributes.size() > 0) {
return extractBeanValues(pConverter, pValue, pPathParts, attributes);
} else {
// No further attributes, return string representation
return pValue.toString();
}
}
}
private Object extractBeanValues(ObjectToJsonConverter pConverter, Object pValue, Stack<String> pPathParts, List<String> pAttributes) throws AttributeNotFoundException {
Map ret = new JSONObject();
for (String attribute : pAttributes) {
Stack path = (Stack) pPathParts.clone();
try {
ret.put(attribute, extractJsonifiedPropertyValue(pConverter, pValue, attribute, path));
} catch (ValueFaultHandler.AttributeFilteredException exp) {
// Skip it since we are doing a path with wildcards, filtering out non-matchin attrs.
}
}
if (ret.isEmpty() && pAttributes.size() > 0) {
// Ok, everything was filtered. Bubbling upwards ...
throw new ValueFaultHandler.AttributeFilteredException();
}
return ret;
}
@SuppressWarnings("PMD.CompareObjectsWithEquals")
private Object extractJsonifiedPropertyValue(ObjectToJsonConverter pConverter, Object pValue, String pAttribute, Stack<String> pPathParts)
throws AttributeNotFoundException {
ValueFaultHandler faultHandler = pConverter.getValueFaultHandler();
Object value = extractBeanPropertyValue(pValue, pAttribute, faultHandler);
if (value == null) {
if (!pPathParts.isEmpty()) {
faultHandler.handleException(new AttributeNotFoundException(
"Cannot apply remaining path " + EscapeUtil.combineToPath(pPathParts) + " on value null"));
}
return null;
} else if (value == pValue) {
if (!pPathParts.isEmpty()) {
faultHandler.handleException(new AttributeNotFoundException(
"Cannot apply remaining path " + EscapeUtil.combineToPath(pPathParts) + " on a cycle"));
}
// Break Cycle
return "[this]";
} else {
// Call into the converted recursively for any object known.
return pConverter.extractObject(value, pPathParts, true /* jsonify */);
}
}
// Extract all attributes from a given bean
private List<String> extractBeanAttributes(Object pValue) {
List<String> attrs = new ArrayList<String>();
for (Method method : pValue.getClass().getMethods()) {
if (!Modifier.isStatic(method.getModifiers()) &&
!IGNORE_METHODS.contains(method.getName()) &&
!isIgnoredType(method.getReturnType()) &&
!hasAnnotation(method, "java.beans.Transient")) {
addAttributes(attrs, method);
}
}
return attrs;
}
private boolean hasAnnotation(Method method, String annotation) {
for (Annotation anno : method.getAnnotations()) {
if (anno.annotationType().getName().equals(annotation)) {
return true;
}
}
return false;
}
// Add attributes, which are taken from get methods to the given list
@SuppressWarnings("PMD.UnnecessaryCaseChange")
private void addAttributes(List<String> pAttrs, Method pMethod) {
String name = pMethod.getName();
for (String pref : GETTER_PREFIX) {
if (name.startsWith(pref) && name.length() > pref.length()
&& pMethod.getParameterTypes().length == 0) {
int len = pref.length();
String firstLetter = name.substring(len,len+1);
// Only for getter compliant to the beans conventions (first letter after prefix is upper case)
if (firstLetter.toUpperCase().equals(firstLetter)) {
String attribute =
new StringBuffer(firstLetter.toLowerCase()).
append(name.substring(len+1)).toString();
pAttrs.add(attribute);
}
}
}
}
private Object extractBeanPropertyValue(Object pValue, String pAttribute, ValueFaultHandler pFaultHandler)
throws AttributeNotFoundException {
Class clazz = pValue.getClass();
Method method = null;
String suffix = new StringBuilder(pAttribute.substring(0,1).toUpperCase()).append(pAttribute.substring(1)).toString();
for (String pref : GETTER_PREFIX) {
try {
String methodName = new StringBuilder(pref).append(suffix).toString();
method = clazz.getMethod(methodName);
} catch (NoSuchMethodException e) {
// Try next one
continue;
}
// We found a valid method
break;
}
// Finally, try the attribute name directly
if (method == null) {
try {
method = clazz.getMethod(new StringBuilder(pAttribute.substring(0,1).toLowerCase())
.append(pAttribute.substring(1)).toString());
} catch (NoSuchMethodException exp) {
method = null;
}
}
if (method == null) {
return pFaultHandler.handleException(new AttributeNotFoundException(
"No getter known for attribute " + pAttribute + " for class " + pValue.getClass().getName()));
}
try {
method.setAccessible(true);
return method.invoke(pValue);
} catch (IllegalAccessException e) {
return pFaultHandler.handleException(new IllegalStateException("Error while extracting " + pAttribute
+ " from " + pValue,e));
} catch (InvocationTargetException e) {
return pFaultHandler.handleException(new IllegalStateException("Error while extracting " + pAttribute
+ " from " + pValue,e));
}
}
/**
* Privileged action for setting the accesibility mode for a method to true
*/
private static class SetMethodAccessibleAction implements PrivilegedAction<Void> {
private final Method getMethod;
/**
* Which method to set accessible
*
* @param pMethod method to set accessible
*/
public SetMethodAccessibleAction(Method pMethod) {
getMethod = pMethod;
}
/** {@inheritDoc} */
public Void run() {
getMethod.setAccessible(true);
return null;
}
}
// Ignore certain return types, since their getter tend to have bad
// side effects like nuking files etc. See Jetty FileResource.getOutputStream() as a bad example
// This method is not necessarily cheap (since it called quite often), however necessary
// as safety net. I messed up my complete local Maven repository only be serializing a Jetty ServletContext
private boolean isIgnoredType(Class<?> pReturnType) {
for (Class<?> type : IGNORED_RETURN_TYPES) {
if (type.isAssignableFrom(pReturnType)) {
return true;
}
}
return false;
}
}