Save This Page
Home » struts-2.1.8.1-src » org.apache » struts2 » json » [javadoc | source]
    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   }

Save This Page
Home » struts-2.1.8.1-src » org.apache » struts2 » json » [javadoc | source]