View Javadoc

1   /*
2    * $Id: JSONInterceptor.java 799110 2009-07-29 22:44:26Z musachy $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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             // load JSON object
105             Object obj = JSONUtil.deserialize(request.getReader());
106 
107             if (obj instanceof Map) {
108                 Map json = (Map) obj;
109 
110                 // clean up the values
111                 if (dataCleaner != null)
112                     dataCleaner.clean("", json);
113 
114                 // populate fields
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                 // load JSON object
124                 Object obj = JSONUtil.deserialize(request.getReader());
125 
126                 if (obj instanceof Map) {
127                     Map smd = (Map) obj;
128 
129                     // invoke method
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         // validate id
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         // could be a numeric value
197         response.setId(id.toString());
198 
199         // the map is going to have: 'params', 'method' and 'id' (what is the id
200         // for?)
201         Class clazz = object.getClass();
202 
203         // parameters
204         List parameters = (List) data.get("params");
205         int parameterCount = parameters != null ? parameters.size() : 0;
206 
207         // method
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         // parameters
223         if (parameterCount > 0) {
224             Class[] parameterTypes = method.getParameterTypes();
225             Type[] genericTypes = method.getGenericParameterTypes();
226             List invocationParameters = new ArrayList();
227 
228             // validate size
229             if (parameterTypes.length != parameterCount) {
230                 // size mismatch
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             // convert parameters
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                 // clean up the values
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 }