1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.apache.struts2.json;
22
23 import java.beans.IntrospectionException;
24 import java.lang.reflect.InvocationTargetException;
25 import java.lang.reflect.Method;
26 import java.lang.reflect.Type;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.regex.Pattern;
31
32 import javax.servlet.http.HttpServletRequest;
33 import javax.servlet.http.HttpServletResponse;
34
35 import org.apache.struts2.ServletActionContext;
36 import org.apache.struts2.StrutsConstants;
37 import org.apache.struts2.json.annotations.SMDMethod;
38 import org.apache.struts2.json.rpc.RPCError;
39 import org.apache.struts2.json.rpc.RPCErrorCode;
40 import org.apache.struts2.json.rpc.RPCResponse;
41
42 import com.opensymphony.xwork2.Action;
43 import com.opensymphony.xwork2.ActionInvocation;
44 import com.opensymphony.xwork2.inject.Inject;
45 import com.opensymphony.xwork2.interceptor.Interceptor;
46 import com.opensymphony.xwork2.util.ValueStack;
47 import com.opensymphony.xwork2.util.logging.Logger;
48 import com.opensymphony.xwork2.util.logging.LoggerFactory;
49
50 /***
51 * Populates an action from a JSON string
52 */
53 public class JSONInterceptor implements Interceptor {
54 private static final long serialVersionUID = 4950170304212158803L;
55 private static final Logger LOG = LoggerFactory.getLogger(JSONInterceptor.class);
56 private boolean enableSMD = false;
57 private boolean enableGZIP = false;
58 private boolean wrapWithComments;
59 private boolean prefix;
60 private String defaultEncoding = "ISO-8859-1";
61 private boolean ignoreHierarchy = true;
62 private String root;
63 private List<Pattern> excludeProperties;
64 private List<Pattern> includeProperties;
65 private boolean ignoreSMDMethodInterfaces = true;
66 private JSONPopulator populator = new JSONPopulator();
67 private JSONCleaner dataCleaner = null;
68 private boolean debug = false;
69 private boolean noCache = false;
70 private boolean excludeNullProperties;
71 private String callbackParameter;
72 private String contentType;
73
74 public void destroy() {
75 }
76
77 public void init() {
78 }
79
80 @SuppressWarnings("unchecked")
81 public String intercept(ActionInvocation invocation) throws Exception {
82 HttpServletRequest request = ServletActionContext.getRequest();
83 HttpServletResponse response = ServletActionContext.getResponse();
84 String contentType = request.getHeader("content-type");
85 if (contentType != null) {
86 int iSemicolonIdx;
87 if ((iSemicolonIdx = contentType.indexOf(";")) != -1)
88 contentType = contentType.substring(0, iSemicolonIdx);
89 }
90
91 Object rootObject;
92 if (this.root != null) {
93 ValueStack stack = invocation.getStack();
94 rootObject = stack.findValue(this.root);
95
96 if (rootObject == null) {
97 throw new RuntimeException("Invalid root expression: '" + this.root + "'.");
98 }
99 } else {
100 rootObject = invocation.getAction();
101 }
102
103 if ((contentType != null) && contentType.equalsIgnoreCase("application/json")) {
104
105 Object obj = JSONUtil.deserialize(request.getReader());
106
107 if (obj instanceof Map) {
108 Map json = (Map) obj;
109
110
111 if (dataCleaner != null)
112 dataCleaner.clean("", json);
113
114
115 populator.populateObject(rootObject, json);
116 } else {
117 LOG.error("Unable to deserialize JSON object from request");
118 throw new JSONException("Unable to deserialize JSON object from request");
119 }
120 } else if ((contentType != null) && contentType.equalsIgnoreCase("application/json-rpc")) {
121 Object result;
122 if (this.enableSMD) {
123
124 Object obj = JSONUtil.deserialize(request.getReader());
125
126 if (obj instanceof Map) {
127 Map smd = (Map) obj;
128
129
130 try {
131 result = this.invoke(rootObject, smd);
132 } catch (Exception e) {
133 RPCResponse rpcResponse = new RPCResponse();
134 rpcResponse.setId(smd.get("id").toString());
135 rpcResponse.setError(new RPCError(e, RPCErrorCode.EXCEPTION, debug));
136
137 result = rpcResponse;
138 }
139 } else {
140 String message = "SMD request was not in the right format. See http://json-rpc.org";
141
142 RPCResponse rpcResponse = new RPCResponse();
143 rpcResponse.setError(new RPCError(message, RPCErrorCode.INVALID_PROCEDURE_CALL));
144 result = rpcResponse;
145 }
146
147 String json = JSONUtil.serialize(result, excludeProperties, includeProperties,
148 ignoreHierarchy, excludeNullProperties);
149 json = addCallbackIfApplicable(request, json);
150 JSONUtil.writeJSONToResponse(new SerializationParams(response, this.defaultEncoding,
151 this.wrapWithComments, json, true, false, noCache, -1, -1, prefix, contentType));
152
153 return Action.NONE;
154 } else {
155 String message = "Request with content type of 'application/json-rpc' was received but SMD is "
156 + "not enabled for this interceptor. Set 'enableSMD' to true to enable it";
157
158 RPCResponse rpcResponse = new RPCResponse();
159 rpcResponse.setError(new RPCError(message, RPCErrorCode.SMD_DISABLED));
160 result = rpcResponse;
161 }
162
163 String json = JSONUtil.serialize(result, excludeProperties, includeProperties, ignoreHierarchy,
164 excludeNullProperties);
165 json = addCallbackIfApplicable(request, json);
166 boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request);
167 JSONUtil.writeJSONToResponse(new SerializationParams(response, this.defaultEncoding,
168 this.wrapWithComments, json, true, writeGzip, noCache, -1, -1, prefix, contentType));
169
170 return Action.NONE;
171 } else {
172 if (LOG.isDebugEnabled()) {
173 LOG
174 .debug("Content type must be 'application/json' or 'application/json-rpc'. Ignoring request with content type "
175 + contentType);
176 }
177 }
178
179 return invocation.invoke();
180 }
181
182 @SuppressWarnings("unchecked")
183 public RPCResponse invoke(Object object, Map data) throws IllegalArgumentException,
184 IllegalAccessException, InvocationTargetException, JSONException, InstantiationException,
185 NoSuchMethodException, IntrospectionException {
186
187 RPCResponse response = new RPCResponse();
188
189
190 Object id = data.get("id");
191 if (id == null) {
192 String message = "'id' is required for JSON RPC";
193 response.setError(new RPCError(message, RPCErrorCode.METHOD_NOT_FOUND));
194 return response;
195 }
196
197 response.setId(id.toString());
198
199
200
201 Class clazz = object.getClass();
202
203
204 List parameters = (List) data.get("params");
205 int parameterCount = parameters != null ? parameters.size() : 0;
206
207
208 String methodName = (String) data.get("method");
209 if (methodName == null) {
210 String message = "'method' is required for JSON RPC";
211 response.setError(new RPCError(message, RPCErrorCode.MISSING_METHOD));
212 return response;
213 }
214
215 Method method = this.getMethod(clazz, methodName, parameterCount);
216 if (method == null) {
217 String message = "Method " + methodName + " could not be found in action class.";
218 response.setError(new RPCError(message, RPCErrorCode.METHOD_NOT_FOUND));
219 return response;
220 }
221
222
223 if (parameterCount > 0) {
224 Class[] parameterTypes = method.getParameterTypes();
225 Type[] genericTypes = method.getGenericParameterTypes();
226 List invocationParameters = new ArrayList();
227
228
229 if (parameterTypes.length != parameterCount) {
230
231 String message = "Parameter count in request, " + parameterCount
232 + " do not match expected parameter count for " + methodName + ", "
233 + parameterTypes.length;
234
235 response.setError(new RPCError(message, RPCErrorCode.PARAMETERS_MISMATCH));
236 return response;
237 }
238
239
240 for (int i = 0; i < parameters.size(); i++) {
241 Object parameter = parameters.get(i);
242 Class paramType = parameterTypes[i];
243 Type genericType = genericTypes[i];
244
245
246 if (dataCleaner != null)
247 parameter = dataCleaner.clean("[" + i + "]", parameter);
248
249 Object converted = populator.convert(paramType, genericType, parameter, method);
250 invocationParameters.add(converted);
251 }
252
253 response.setResult(method.invoke(object, invocationParameters.toArray()));
254 } else {
255 response.setResult(method.invoke(object, new Object[0]));
256 }
257
258 return response;
259 }
260
261 @SuppressWarnings("unchecked")
262 private Method getMethod(Class clazz, String name, int parameterCount) {
263 Method[] smdMethods = JSONUtil.listSMDMethods(clazz, ignoreSMDMethodInterfaces);
264
265 for (Method method : smdMethods) {
266 if (checkSMDMethodSignature(method, name, parameterCount)) {
267 return method;
268 }
269 }
270 return null;
271 }
272
273 /***
274 * Look for a method in clazz carrying the SMDMethod annotation with
275 * matching name and parametersCount
276 *
277 * @return true if matches name and parameterCount
278 */
279 private boolean checkSMDMethodSignature(Method method, String name, int parameterCount) {
280
281 SMDMethod smdMethodAnntotation = method.getAnnotation(SMDMethod.class);
282 if (smdMethodAnntotation != null) {
283 String alias = smdMethodAnntotation.name();
284 boolean paramsMatch = method.getParameterTypes().length == parameterCount;
285 if (((alias.length() == 0) && method.getName().equals(name) && paramsMatch)
286 || (alias.equals(name) && paramsMatch)) {
287 return true;
288 }
289 }
290
291 return false;
292 }
293
294 protected String addCallbackIfApplicable(HttpServletRequest request, String json) {
295 if ((callbackParameter != null) && (callbackParameter.length() > 0)) {
296 String callbackName = request.getParameter(callbackParameter);
297 if ((callbackName != null) && (callbackName.length() > 0))
298 json = callbackName + "(" + json + ")";
299 }
300 return json;
301 }
302
303 public boolean isEnableSMD() {
304 return this.enableSMD;
305 }
306
307 public void setEnableSMD(boolean enableSMD) {
308 this.enableSMD = enableSMD;
309 }
310
311 /***
312 * Ignore annotations on methods in interfaces You may need to set to this
313 * true if your action is a proxy/enhanced as annotations are not inherited
314 */
315 public void setIgnoreSMDMethodInterfaces(boolean ignoreSMDMethodInterfaces) {
316 this.ignoreSMDMethodInterfaces = ignoreSMDMethodInterfaces;
317 }
318
319 /***
320 * Wrap generated JSON with comments. Only used if SMD is enabled.
321 *
322 * @param wrapWithComments
323 */
324 public void setWrapWithComments(boolean wrapWithComments) {
325 this.wrapWithComments = wrapWithComments;
326 }
327
328 @Inject(StrutsConstants.STRUTS_I18N_ENCODING)
329 public void setDefaultEncoding(String val) {
330 this.defaultEncoding = val;
331 }
332
333 /***
334 * Ignore properties defined on base classes of the root object.
335 *
336 * @param ignoreHierarchy
337 */
338 public void setIgnoreHierarchy(boolean ignoreHierarchy) {
339 this.ignoreHierarchy = ignoreHierarchy;
340 }
341
342 /***
343 * Sets the root object to be deserialized, defaults to the Action
344 *
345 * @param root
346 * OGNL expression of root object to be serialized
347 */
348 public void setRoot(String root) {
349 this.root = root;
350 }
351
352 /***
353 * Sets the JSONPopulator to be used
354 *
355 * @param populator
356 * JSONPopulator
357 */
358 public void setJSONPopulator(JSONPopulator populator) {
359 this.populator = populator;
360 }
361
362 /***
363 * Sets the JSONCleaner to be used
364 *
365 * @param dataCleaner
366 * JSONCleaner
367 */
368 public void setJSONCleaner(JSONCleaner dataCleaner) {
369 this.dataCleaner = dataCleaner;
370 }
371
372 /***
373 * Turns debugging on or off
374 *
375 * @param debug
376 * true or false
377 */
378 public boolean getDebug() {
379 return this.debug;
380 }
381
382 public void setDebug(boolean debug) {
383 this.debug = debug;
384 }
385
386 /***
387 * Sets a comma-delimited list of regular expressions to match properties
388 * that should be excluded from the JSON output.
389 *
390 * @param commaDelim
391 * A comma-delimited list of regular expressions
392 */
393 public void setExcludeProperties(String commaDelim) {
394 List<String> excludePatterns = JSONUtil.asList(commaDelim);
395 if (excludePatterns != null) {
396 this.excludeProperties = new ArrayList<Pattern>(excludePatterns.size());
397 for (String pattern : excludePatterns) {
398 this.excludeProperties.add(Pattern.compile(pattern));
399 }
400 }
401 }
402
403 /***
404 * Sets a comma-delimited list of regular expressions to match properties
405 * that should be included from the JSON output.
406 *
407 * @param commaDelim
408 * A comma-delimited list of regular expressions
409 */
410 public void setIncludeProperties(String commaDelim) {
411 List<String> includePatterns = JSONUtil.asList(commaDelim);
412 if (includePatterns != null) {
413 this.includeProperties = new ArrayList<Pattern>(includePatterns.size());
414 for (String pattern : includePatterns) {
415 this.includeProperties.add(Pattern.compile(pattern));
416 }
417 }
418 }
419
420 public boolean isEnableGZIP() {
421 return enableGZIP;
422 }
423
424 /***
425 * Setting this property to "true" will compress the output.
426 *
427 * @param enableGZIP
428 * Enable compressed output
429 */
430 public void setEnableGZIP(boolean enableGZIP) {
431 this.enableGZIP = enableGZIP;
432 }
433
434 public boolean isNoCache() {
435 return noCache;
436 }
437
438 /***
439 * Add headers to response to prevent the browser from caching the response
440 *
441 * @param noCache
442 */
443 public void setNoCache(boolean noCache) {
444 this.noCache = noCache;
445 }
446
447 public boolean isExcludeNullProperties() {
448 return excludeNullProperties;
449 }
450
451 /***
452 * Do not serialize properties with a null value
453 *
454 * @param excludeNullProperties
455 */
456 public void setExcludeNullProperties(boolean excludeNullProperties) {
457 this.excludeNullProperties = excludeNullProperties;
458 }
459
460 public void setCallbackParameter(String callbackParameter) {
461 this.callbackParameter = callbackParameter;
462 }
463
464 public String getCallbackParameter() {
465 return callbackParameter;
466 }
467
468 /***
469 * Add "{} && " to generated JSON
470 *
471 * @param prefix
472 */
473 public void setPrefix(boolean prefix) {
474 this.prefix = prefix;
475 }
476
477 public void setContentType(String contentType) {
478 this.contentType = contentType;
479 }
480 }