View Javadoc

1   /*
2    * $Id: JSONResult.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.io.IOException;
24  import java.lang.annotation.Annotation;
25  import java.lang.reflect.Method;
26  import java.util.ArrayList;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.regex.Pattern;
30  
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpServletResponse;
33  
34  import org.apache.struts2.StrutsConstants;
35  import org.apache.struts2.StrutsStatics;
36  import org.apache.struts2.json.annotations.SMD;
37  import org.apache.struts2.json.annotations.SMDMethod;
38  import org.apache.struts2.json.annotations.SMDMethodParameter;
39  
40  import com.opensymphony.xwork2.ActionContext;
41  import com.opensymphony.xwork2.ActionInvocation;
42  import com.opensymphony.xwork2.Result;
43  import com.opensymphony.xwork2.inject.Inject;
44  import com.opensymphony.xwork2.util.ValueStack;
45  import com.opensymphony.xwork2.util.logging.Logger;
46  import com.opensymphony.xwork2.util.logging.LoggerFactory;
47  
48  /***
49   * <!-- START SNIPPET: description --> <p/> This result serializes an action
50   * into JSON. <p/> <!-- END SNIPPET: description --> <p/> <p/> <u>Result
51   * parameters:</u> <p/> <!-- START SNIPPET: parameters --> <p/>
52   * <ul>
53   * <p/>
54   * <li>excludeProperties - list of regular expressions matching the properties
55   * to be excluded. The regular expressions are evaluated against the OGNL
56   * expression representation of the properties. </li>
57   * <p/>
58   * </ul>
59   * <p/> <!-- END SNIPPET: parameters --> <p/> <b>Example:</b> <p/>
60   * 
61   * <pre>
62   * &lt;!-- START SNIPPET: example --&gt;
63   * &lt;result name=&quot;success&quot; type=&quot;json&quot; /&gt;
64   * &lt;!-- END SNIPPET: example --&gt;
65   * </pre>
66   */
67  public class JSONResult implements Result {
68      private static final long serialVersionUID = 8624350183189931165L;
69      private static final Logger LOG = LoggerFactory.getLogger(JSONResult.class);
70  
71      private String defaultEncoding = "ISO-8859-1";
72      private List<Pattern> includeProperties;
73      private List<Pattern> excludeProperties;
74      private String root;
75      private boolean wrapWithComments;
76      private boolean prefix;
77      private boolean enableSMD = false;
78      private boolean enableGZIP = false;
79      private boolean ignoreHierarchy = true;
80      private boolean ignoreInterfaces = true;
81      private boolean enumAsBean = JSONWriter.ENUM_AS_BEAN_DEFAULT;
82      private boolean noCache = false;
83      private boolean excludeNullProperties = false;
84      private int statusCode;
85      private int errorCode;
86      private String callbackParameter;
87      private String contentType;
88      private String wrapPrefix;
89      private String wrapSuffix;
90  
91      @Inject(StrutsConstants.STRUTS_I18N_ENCODING)
92      public void setDefaultEncoding(String val) {
93          this.defaultEncoding = val;
94      }
95  
96      /***
97       * Gets a list of regular expressions of properties to exclude from the JSON
98       * output.
99       * 
100      * @return A list of compiled regular expression patterns
101      */
102     public List<Pattern> getExcludePropertiesList() {
103         return this.excludeProperties;
104     }
105 
106     /***
107      * Sets a comma-delimited list of regular expressions to match properties
108      * that should be excluded from the JSON output.
109      * 
110      * @param commaDelim
111      *            A comma-delimited list of regular expressions
112      */
113     public void setExcludeProperties(String commaDelim) {
114         List<String> excludePatterns = JSONUtil.asList(commaDelim);
115         if (excludePatterns != null) {
116             this.excludeProperties = new ArrayList<Pattern>(excludePatterns.size());
117             for (String pattern : excludePatterns) {
118                 this.excludeProperties.add(Pattern.compile(pattern));
119             }
120         }
121     }
122 
123     /***
124      * @return the includeProperties
125      */
126     public List<Pattern> getIncludePropertiesList() {
127         return includeProperties;
128     }
129 
130     /***
131      * @param includedProperties
132      *            the includeProperties to set
133      */
134     public void setIncludeProperties(String commaDelim) {
135         List<String> includePatterns = JSONUtil.asList(commaDelim);
136         if (includePatterns != null) {
137             this.includeProperties = new ArrayList<Pattern>(includePatterns.size());
138 
139             HashMap existingPatterns = new HashMap();
140 
141             for (String pattern : includePatterns) {
142                 // Compile a pattern for each *unique* "level" of the object
143                 // hierarchy specified in the regex.
144                 String[] patternPieces = pattern.split("//////.");
145 
146                 String patternExpr = "";
147                 for (String patternPiece : patternPieces) {
148                     if (patternExpr.length() > 0) {
149                         patternExpr += "//.";
150                     }
151                     patternExpr += patternPiece;
152 
153                     // Check for duplicate patterns so that there is no overlap.
154                     if (!existingPatterns.containsKey(patternExpr)) {
155                         existingPatterns.put(patternExpr, patternExpr);
156 
157                         // Add a pattern that does not have the indexed property
158                         // matching (ie. list\[\d+\] becomes list).
159                         if (patternPiece.endsWith("//]")) {
160                             this.includeProperties.add(Pattern.compile(patternExpr.substring(0, patternPiece
161                                     .lastIndexOf("//["))));
162 
163                             if (LOG.isDebugEnabled())
164                                 LOG.debug("Adding include property expression:  "
165                                         + patternExpr.substring(0, patternPiece.lastIndexOf("//[")));
166                         }
167 
168                         this.includeProperties.add(Pattern.compile(patternExpr));
169 
170                         if (LOG.isDebugEnabled())
171                             LOG.debug("Adding include property expression:  " + patternExpr);
172                     }
173                 }
174             }
175         }
176     }
177 
178     public void execute(ActionInvocation invocation) throws Exception {
179         ActionContext actionContext = invocation.getInvocationContext();
180         HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);
181         HttpServletResponse response = (HttpServletResponse) actionContext.get(StrutsStatics.HTTP_RESPONSE);
182 
183         try {
184             String json;
185             Object rootObject;
186             if (this.enableSMD) {
187                 // generate SMD
188                 rootObject = this.writeSMD(invocation);
189             } else {
190                 // generate JSON
191                 if (this.root != null) {
192                     ValueStack stack = invocation.getStack();
193                     rootObject = stack.findValue(this.root);
194                 } else {
195                     rootObject = invocation.getAction();
196                 }
197             }
198             json = JSONUtil.serialize(rootObject, excludeProperties, includeProperties, ignoreHierarchy,
199                     enumAsBean, excludeNullProperties);
200             json = addCallbackIfApplicable(request, json);
201 
202             boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request);
203 
204             writeToResponse(response, json, writeGzip);
205 
206         } catch (IOException exception) {
207             LOG.error(exception.getMessage(), exception);
208             throw exception;
209         }
210     }
211 
212     protected void writeToResponse(HttpServletResponse response, String json, boolean gzip)
213             throws IOException {
214         JSONUtil.writeJSONToResponse(new SerializationParams(response, getEncoding(), isWrapWithComments(),
215                 json, false, gzip, noCache, statusCode, errorCode, prefix, contentType, wrapPrefix,
216                 wrapSuffix));
217     }
218 
219     @SuppressWarnings("unchecked")
220     protected org.apache.struts2.json.smd.SMD writeSMD(ActionInvocation invocation) {
221         ActionContext actionContext = invocation.getInvocationContext();
222         HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);
223 
224         // root is based on OGNL expression (action by default)
225         Object rootObject = null;
226         if (this.root != null) {
227             ValueStack stack = invocation.getStack();
228             rootObject = stack.findValue(this.root);
229         } else {
230             rootObject = invocation.getAction();
231         }
232 
233         Class clazz = rootObject.getClass();
234         org.apache.struts2.json.smd.SMD smd = new org.apache.struts2.json.smd.SMD();
235         // URL
236         smd.setServiceUrl(request.getRequestURI());
237 
238         // customize SMD
239         SMD smdAnnotation = (SMD) clazz.getAnnotation(SMD.class);
240         if (smdAnnotation != null) {
241             smd.setObjectName(smdAnnotation.objectName());
242             smd.setServiceType(smdAnnotation.serviceType());
243             smd.setVersion(smdAnnotation.version());
244         }
245 
246         // get public methods
247         Method[] methods = JSONUtil.listSMDMethods(clazz, ignoreInterfaces);
248 
249         for (Method method : methods) {
250             SMDMethod smdMethodAnnotation = method.getAnnotation(SMDMethod.class);
251 
252             // SMDMethod annotation is required
253             if (((smdMethodAnnotation != null) && !this.shouldExcludeProperty(method.getName()))) {
254                 String methodName = smdMethodAnnotation.name().length() == 0 ? method.getName()
255                         : smdMethodAnnotation.name();
256 
257                 org.apache.struts2.json.smd.SMDMethod smdMethod = new org.apache.struts2.json.smd.SMDMethod(
258                         methodName);
259                 smd.addSMDMethod(smdMethod);
260 
261                 // find params for this method
262                 int parametersCount = method.getParameterTypes().length;
263                 if (parametersCount > 0) {
264                     Annotation[][] parameterAnnotations = method.getParameterAnnotations();
265 
266                     for (int i = 0; i < parametersCount; i++) {
267                         // are you ever going to pick shorter names? nope
268                         SMDMethodParameter smdMethodParameterAnnotation = this
269                                 .getSMDMethodParameterAnnotation(parameterAnnotations[i]);
270 
271                         String paramName = smdMethodParameterAnnotation != null ? smdMethodParameterAnnotation
272                                 .name()
273                                 : "p" + i;
274 
275                         // goog thing this is the end of the hierarchy,
276                         // oitherwise I would need that 21'' LCD ;)
277                         smdMethod.addSMDMethodParameter(new org.apache.struts2.json.smd.SMDMethodParameter(
278                                 paramName));
279                     }
280                 }
281 
282             } else {
283                 if (LOG.isDebugEnabled())
284                     LOG.debug("Ignoring property " + method.getName());
285             }
286         }
287         return smd;
288     }
289 
290     /***
291      * Find an SMDethodParameter annotation on this array
292      */
293     private org.apache.struts2.json.annotations.SMDMethodParameter getSMDMethodParameterAnnotation(
294             Annotation[] annotations) {
295         for (Annotation annotation : annotations) {
296             if (annotation instanceof org.apache.struts2.json.annotations.SMDMethodParameter)
297                 return (org.apache.struts2.json.annotations.SMDMethodParameter) annotation;
298         }
299 
300         return null;
301     }
302 
303     private boolean shouldExcludeProperty(String expr) {
304         if (this.excludeProperties != null) {
305             for (Pattern pattern : this.excludeProperties) {
306                 if (pattern.matcher(expr).matches())
307                     return true;
308             }
309         }
310         return false;
311     }
312 
313     /***
314      * Retrieve the encoding <p/>
315      * 
316      * @return The encoding associated with this template (defaults to the value
317      *         of 'struts.i18n.encoding' property)
318      */
319     protected String getEncoding() {
320         String encoding = this.defaultEncoding;
321 
322         if (encoding == null) {
323             encoding = System.getProperty("file.encoding");
324         }
325 
326         if (encoding == null) {
327             encoding = "UTF-8";
328         }
329 
330         return encoding;
331     }
332 
333     protected String addCallbackIfApplicable(HttpServletRequest request, String json) {
334         if ((callbackParameter != null) && (callbackParameter.length() > 0)) {
335             String callbackName = request.getParameter(callbackParameter);
336             if ((callbackName != null) && (callbackName.length() > 0))
337                 json = callbackName + "(" + json + ")";
338         }
339         return json;
340     }
341 
342     /***
343      * @return OGNL expression of root object to be serialized
344      */
345     public String getRoot() {
346         return this.root;
347     }
348 
349     /***
350      * Sets the root object to be serialized, defaults to the Action
351      * 
352      * @param root
353      *            OGNL expression of root object to be serialized
354      */
355     public void setRoot(String root) {
356         this.root = root;
357     }
358 
359     /***
360      * @return Generated JSON must be enclosed in comments
361      */
362     public boolean isWrapWithComments() {
363         return this.wrapWithComments;
364     }
365 
366     /***
367      * Wrap generated JSON with comments
368      * 
369      * @param wrapWithComments
370      */
371     public void setWrapWithComments(boolean wrapWithComments) {
372         this.wrapWithComments = wrapWithComments;
373     }
374 
375     /***
376      * @return Result has SMD generation enabled
377      */
378     public boolean isEnableSMD() {
379         return this.enableSMD;
380     }
381 
382     /***
383      * Enable SMD generation for action, which can be used for JSON-RPC
384      * 
385      * @param enableSMD
386      */
387     public void setEnableSMD(boolean enableSMD) {
388         this.enableSMD = enableSMD;
389     }
390 
391     public void setIgnoreHierarchy(boolean ignoreHierarchy) {
392         this.ignoreHierarchy = ignoreHierarchy;
393     }
394 
395     /***
396      * Controls whether interfaces should be inspected for method annotations
397      * You may need to set to this true if your action is a proxy as annotations
398      * on methods are not inherited
399      */
400     public void setIgnoreInterfaces(boolean ignoreInterfaces) {
401         this.ignoreInterfaces = ignoreInterfaces;
402     }
403 
404     /***
405      * Controls how Enum's are serialized : If true, an Enum is serialized as a
406      * name=value pair (name=name()) (default) If false, an Enum is serialized
407      * as a bean with a special property _name=name()
408      * 
409      * @param enumAsBean
410      */
411     public void setEnumAsBean(boolean enumAsBean) {
412         this.enumAsBean = enumAsBean;
413     }
414 
415     public boolean isEnumAsBean() {
416         return enumAsBean;
417     }
418 
419     public boolean isEnableGZIP() {
420         return enableGZIP;
421     }
422 
423     public void setEnableGZIP(boolean enableGZIP) {
424         this.enableGZIP = enableGZIP;
425     }
426 
427     public boolean isNoCache() {
428         return noCache;
429     }
430 
431     /***
432      * Add headers to response to prevent the browser from caching the response
433      * 
434      * @param noCache
435      */
436     public void setNoCache(boolean noCache) {
437         this.noCache = noCache;
438     }
439 
440     public boolean isIgnoreHierarchy() {
441         return ignoreHierarchy;
442     }
443 
444     public boolean isExcludeNullProperties() {
445         return excludeNullProperties;
446     }
447 
448     /***
449      * Do not serialize properties with a null value
450      * 
451      * @param excludeNullProperties
452      */
453     public void setExcludeNullProperties(boolean excludeNullProperties) {
454         this.excludeNullProperties = excludeNullProperties;
455     }
456 
457     /***
458      * Status code to be set in the response
459      * 
460      * @param statusCode
461      */
462     public void setStatusCode(int statusCode) {
463         this.statusCode = statusCode;
464     }
465 
466     /***
467      * Error code to be set in the response
468      * 
469      * @param errorCode
470      */
471     public void setErrorCode(int errorCode) {
472         this.errorCode = errorCode;
473     }
474 
475     public void setCallbackParameter(String callbackParameter) {
476         this.callbackParameter = callbackParameter;
477     }
478 
479     public String getCallbackParameter() {
480         return callbackParameter;
481     }
482 
483     /***
484      * Prefix JSON with "{} &&"
485      * 
486      * @param prefix
487      */
488     public void setPrefix(boolean prefix) {
489         this.prefix = prefix;
490     }
491 
492     /***
493      * Content type to be set in the response
494      * 
495      * @param contentType
496      */
497     public void setContentType(String contentType) {
498         this.contentType = contentType;
499     }
500 
501     public String getWrapPrefix() {
502         return wrapPrefix;
503     }
504 
505     /***
506      * Text to be inserted at the begining of the response
507      */
508     public void setWrapPrefix(String wrapPrefix) {
509         this.wrapPrefix = wrapPrefix;
510     }
511 
512     public String getWrapSuffix() {
513         return wrapSuffix;
514     }
515 
516     /***
517      * Text to be inserted at the end of the response
518      */
519     public void setWrapSuffix(String wrapSuffix) {
520         this.wrapSuffix = wrapSuffix;
521     }
522 }