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.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 * <!-- START SNIPPET: example -->
63 * <result name="success" type="json" />
64 * <!-- END SNIPPET: example -->
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
143
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
154 if (!existingPatterns.containsKey(patternExpr)) {
155 existingPatterns.put(patternExpr, patternExpr);
156
157
158
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
188 rootObject = this.writeSMD(invocation);
189 } else {
190
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
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
236 smd.setServiceUrl(request.getRequestURI());
237
238
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
247 Method[] methods = JSONUtil.listSMDMethods(clazz, ignoreInterfaces);
248
249 for (Method method : methods) {
250 SMDMethod smdMethodAnnotation = method.getAnnotation(SMDMethod.class);
251
252
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
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
268 SMDMethodParameter smdMethodParameterAnnotation = this
269 .getSMDMethodParameterAnnotation(parameterAnnotations[i]);
270
271 String paramName = smdMethodParameterAnnotation != null ? smdMethodParameterAnnotation
272 .name()
273 : "p" + i;
274
275
276
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 }