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.BeanInfo;
24 import java.beans.Introspector;
25 import java.beans.PropertyDescriptor;
26 import java.lang.reflect.Array;
27 import java.lang.reflect.Method;
28 import java.text.CharacterIterator;
29 import java.text.DateFormat;
30 import java.text.SimpleDateFormat;
31 import java.text.StringCharacterIterator;
32 import java.util.Calendar;
33 import java.util.Collection;
34 import java.util.Date;
35 import java.util.Iterator;
36 import java.util.Locale;
37 import java.util.Map;
38 import java.util.Stack;
39 import java.util.regex.Pattern;
40
41 import org.apache.struts2.json.annotations.JSON;
42
43 import com.opensymphony.xwork2.util.logging.Logger;
44 import com.opensymphony.xwork2.util.logging.LoggerFactory;
45
46 /***
47 * <p>
48 * Serializes an object into JavaScript Object Notation (JSON). If cyclic
49 * references are detected they will be nulled out.
50 * </p>
51 */
52 class JSONWriter {
53 private static final Logger LOG = LoggerFactory.getLogger(JSONWriter.class);
54
55 /***
56 * By default, enums are serialzied as name=value pairs
57 */
58 public static final boolean ENUM_AS_BEAN_DEFAULT = false;
59
60 static char[] hex = "0123456789ABCDEF".toCharArray();
61 private StringBuilder buf = new StringBuilder();
62 private Stack stack = new Stack();
63 private boolean ignoreHierarchy = true;
64 private Object root;
65 private boolean buildExpr = true;
66 private String exprStack = "";
67 private Collection<Pattern> excludeProperties;
68 private Collection<Pattern> includeProperties;
69 private DateFormat formatter;
70 private boolean enumAsBean = ENUM_AS_BEAN_DEFAULT;
71 private boolean excludeNullProperties;
72
73 /***
74 * @param object
75 * Object to be serialized into JSON
76 * @return JSON string for object
77 * @throws JSONException
78 */
79 public String write(Object object) throws JSONException {
80 return this.write(object, null, null, false);
81 }
82
83 /***
84 * @param object
85 * Object to be serialized into JSON
86 * @return JSON string for object
87 * @throws JSONException
88 */
89 public String write(Object object, Collection<Pattern> excludeProperties,
90 Collection<Pattern> includeProperties, boolean excludeNullProperties) throws JSONException {
91 this.excludeNullProperties = excludeNullProperties;
92 this.buf.setLength(0);
93 this.root = object;
94 this.exprStack = "";
95 this.buildExpr = ((excludeProperties != null) && !excludeProperties.isEmpty())
96 || ((includeProperties != null) && !includeProperties.isEmpty());
97 this.excludeProperties = excludeProperties;
98 this.includeProperties = includeProperties;
99 this.value(object, null);
100
101 return this.buf.toString();
102 }
103
104 /***
105 * Detect cyclic references
106 */
107 private void value(Object object, Method method) throws JSONException {
108 if (object == null) {
109 this.add("null");
110
111 return;
112 }
113
114 if (this.stack.contains(object)) {
115 Class clazz = object.getClass();
116
117
118 if (clazz.isPrimitive() || clazz.equals(String.class)) {
119 this.process(object, method);
120 } else {
121 if (LOG.isDebugEnabled()) {
122 LOG.debug("Cyclic reference detected on " + object);
123 }
124
125 this.add("null");
126 }
127
128 return;
129 }
130
131 this.process(object, method);
132 }
133
134 /***
135 * Serialize object into json
136 */
137 private void process(Object object, Method method) throws JSONException {
138 this.stack.push(object);
139
140 if (object instanceof Class) {
141 this.string(object);
142 } else if (object instanceof Boolean) {
143 this.bool(((Boolean) object).booleanValue());
144 } else if (object instanceof Number) {
145 this.add(object);
146 } else if (object instanceof String) {
147 this.string(object);
148 } else if (object instanceof Character) {
149 this.string(object);
150 } else if (object instanceof Map) {
151 this.map((Map) object, method);
152 } else if (object.getClass().isArray()) {
153 this.array(object, method);
154 } else if (object instanceof Iterable) {
155 this.array(((Iterable) object).iterator(), method);
156 } else if (object instanceof Date) {
157 this.date((Date) object, method);
158 } else if (object instanceof Calendar) {
159 this.date(((Calendar) object).getTime(), method);
160 } else if (object instanceof Locale) {
161 this.string(object);
162 } else if (object instanceof Enum) {
163 this.enumeration((Enum) object);
164 } else {
165 this.bean(object);
166 }
167
168 this.stack.pop();
169 }
170
171 /***
172 * Instrospect bean and serialize its properties
173 */
174 private void bean(Object object) throws JSONException {
175 this.add("{");
176
177 BeanInfo info;
178
179 try {
180 Class clazz = object.getClass();
181
182 info = ((object == this.root) && this.ignoreHierarchy) ? Introspector.getBeanInfo(clazz, clazz
183 .getSuperclass()) : Introspector.getBeanInfo(clazz);
184
185 PropertyDescriptor[] props = info.getPropertyDescriptors();
186
187 boolean hasData = false;
188 for (int i = 0; i < props.length; ++i) {
189 PropertyDescriptor prop = props[i];
190 String name = prop.getName();
191 Method accessor = prop.getReadMethod();
192 Method baseAccessor = null;
193 if (clazz.getName().indexOf("$$EnhancerByCGLIB$$") > -1) {
194 try {
195 baseAccessor = Class.forName(
196 clazz.getName().substring(0, clazz.getName().indexOf("$$"))).getMethod(
197 accessor.getName(), accessor.getParameterTypes());
198 } catch (Exception ex) {
199 LOG.debug(ex.getMessage(), ex);
200 }
201 } else
202 baseAccessor = accessor;
203
204 if (baseAccessor != null) {
205
206 JSON json = baseAccessor.getAnnotation(JSON.class);
207 if (json != null) {
208 if (!json.serialize())
209 continue;
210 else if (json.name().length() > 0)
211 name = json.name();
212 }
213
214
215 if (this.shouldExcludeProperty(clazz, prop)) {
216 continue;
217 }
218 String expr = null;
219 if (this.buildExpr) {
220 expr = this.expandExpr(name);
221 if (this.shouldExcludeProperty(expr)) {
222 continue;
223 }
224 expr = this.setExprStack(expr);
225 }
226
227 Object value = accessor.invoke(object, new Object[0]);
228 boolean propertyPrinted = this.add(name, value, accessor, hasData);
229 hasData = hasData || propertyPrinted;
230 if (this.buildExpr) {
231 this.setExprStack(expr);
232 }
233 }
234 }
235
236
237
238 if (object instanceof Enum) {
239 Object value = ((Enum) object).name();
240 this.add("_name", value, object.getClass().getMethod("name"), hasData);
241 }
242 } catch (Exception e) {
243 throw new JSONException(e);
244 }
245
246 this.add("}");
247 }
248
249 /***
250 * Instrospect an Enum and serialize it as a name/value pair or as a bean
251 * including all its own properties
252 */
253 private void enumeration(Enum enumeration) throws JSONException {
254 if (enumAsBean) {
255 this.bean(enumeration);
256 } else {
257 this.string(enumeration.name());
258 }
259 }
260
261 /***
262 * Ignore "class" field
263 */
264 private boolean shouldExcludeProperty(Class clazz, PropertyDescriptor prop) throws SecurityException,
265 NoSuchFieldException {
266 String name = prop.getName();
267
268 if (name.equals("class") || name.equals("declaringClass") || name.equals("cachedSuperClass")
269 || name.equals("metaClass")) {
270 return true;
271 }
272
273 return false;
274 }
275
276 private String expandExpr(int i) {
277 return this.exprStack + "[" + i + "]";
278 }
279
280 private String expandExpr(String property) {
281 if (this.exprStack.length() == 0)
282 return property;
283 return this.exprStack + "." + property;
284 }
285
286 private String setExprStack(String expr) {
287 String s = this.exprStack;
288 this.exprStack = expr;
289 return s;
290 }
291
292 private boolean shouldExcludeProperty(String expr) {
293 if (this.excludeProperties != null) {
294 for (Pattern pattern : this.excludeProperties) {
295 if (pattern.matcher(expr).matches()) {
296 if (LOG.isDebugEnabled())
297 LOG.debug("Ignoring property because of exclude rule: " + expr);
298 return true;
299 }
300 }
301 }
302
303 if (this.includeProperties != null) {
304 for (Pattern pattern : this.includeProperties) {
305 if (pattern.matcher(expr).matches()) {
306 return false;
307 }
308 }
309
310 if (LOG.isDebugEnabled())
311 LOG.debug("Ignoring property because of include rule: " + expr);
312 return true;
313 }
314
315 return false;
316 }
317
318 /***
319 * Add name/value pair to buffer
320 */
321 private boolean add(String name, Object value, Method method, boolean hasData) throws JSONException {
322 if (!excludeNullProperties || (value != null)) {
323 if (hasData) {
324 this.add(',');
325 }
326 this.add('"');
327 this.add(name);
328 this.add("\":");
329 this.value(value, method);
330 return true;
331 }
332
333 return false;
334 }
335
336 /***
337 * Add map to buffer
338 */
339 private void map(Map map, Method method) throws JSONException {
340 this.add("{");
341
342 Iterator it = map.entrySet().iterator();
343
344 boolean warnedNonString = false;
345 boolean hasData = false;
346 while (it.hasNext()) {
347 Map.Entry entry = (Map.Entry) it.next();
348 Object key = entry.getKey();
349 String expr = null;
350 if (this.buildExpr) {
351 if (key == null) {
352 LOG.error("Cannot build expression for null key in " + this.exprStack);
353 continue;
354 } else {
355 expr = this.expandExpr(key.toString());
356 if (this.shouldExcludeProperty(expr)) {
357 continue;
358 }
359 expr = this.setExprStack(expr);
360 }
361 }
362 if (hasData) {
363 this.add(',');
364 }
365 hasData = true;
366 if (!warnedNonString && !(key instanceof String)) {
367 LOG.warn("JavaScript doesn't support non-String keys, using toString() on "
368 + key.getClass().getName());
369 warnedNonString = true;
370 }
371 this.value(key.toString(), method);
372 this.add(":");
373 this.value(entry.getValue(), method);
374 if (this.buildExpr) {
375 this.setExprStack(expr);
376 }
377 }
378
379 this.add("}");
380 }
381
382 /***
383 * Add date to buffer
384 */
385 private void date(Date date, Method method) {
386 JSON json = null;
387 if (method != null)
388 json = method.getAnnotation(JSON.class);
389 if (this.formatter == null)
390 this.formatter = new SimpleDateFormat(JSONUtil.RFC3339_FORMAT);
391
392 DateFormat formatter = (json != null) && (json.format().length() > 0) ? new SimpleDateFormat(json
393 .format()) : this.formatter;
394 this.string(formatter.format(date));
395 }
396
397 /***
398 * Add array to buffer
399 */
400 private void array(Iterator it, Method method) throws JSONException {
401 this.add("[");
402
403 boolean hasData = false;
404 for (int i = 0; it.hasNext(); i++) {
405 String expr = null;
406 if (this.buildExpr) {
407 expr = this.expandExpr(i);
408 if (this.shouldExcludeProperty(expr)) {
409 it.next();
410 continue;
411 }
412 expr = this.setExprStack(expr);
413 }
414 if (hasData) {
415 this.add(',');
416 }
417 hasData = true;
418 this.value(it.next(), method);
419 if (this.buildExpr) {
420 this.setExprStack(expr);
421 }
422 }
423
424 this.add("]");
425 }
426
427 /***
428 * Add array to buffer
429 */
430 private void array(Object object, Method method) throws JSONException {
431 this.add("[");
432
433 int length = Array.getLength(object);
434
435 boolean hasData = false;
436 for (int i = 0; i < length; ++i) {
437 String expr = null;
438 if (this.buildExpr) {
439 expr = this.expandExpr(i);
440 if (this.shouldExcludeProperty(expr)) {
441 continue;
442 }
443 expr = this.setExprStack(expr);
444 }
445 if (hasData) {
446 this.add(',');
447 }
448 hasData = true;
449 this.value(Array.get(object, i), method);
450 if (this.buildExpr) {
451 this.setExprStack(expr);
452 }
453 }
454
455 this.add("]");
456 }
457
458 /***
459 * Add boolean to buffer
460 */
461 private void bool(boolean b) {
462 this.add(b ? "true" : "false");
463 }
464
465 /***
466 * escape characters
467 */
468 private void string(Object obj) {
469 this.add('"');
470
471 CharacterIterator it = new StringCharacterIterator(obj.toString());
472
473 for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
474 if (c == '"') {
475 this.add("//\"");
476 } else if (c == '//') {
477 this.add("////");
478 } else if (c == '/') {
479 this.add("///");
480 } else if (c == '\b') {
481 this.add("//b");
482 } else if (c == '\f') {
483 this.add("//f");
484 } else if (c == '\n') {
485 this.add("//n");
486 } else if (c == '\r') {
487 this.add("//r");
488 } else if (c == '\t') {
489 this.add("//t");
490 } else if (Character.isISOControl(c)) {
491 this.unicode(c);
492 } else {
493 this.add(c);
494 }
495 }
496
497 this.add('"');
498 }
499
500 /***
501 * Add object to buffer
502 */
503 private void add(Object obj) {
504 this.buf.append(obj);
505 }
506
507 /***
508 * Add char to buffer
509 */
510 private void add(char c) {
511 this.buf.append(c);
512 }
513
514 /***
515 * Represent as unicode
516 *
517 * @param c
518 * character to be encoded
519 */
520 private void unicode(char c) {
521 this.add("//u");
522
523 int n = c;
524
525 for (int i = 0; i < 4; ++i) {
526 int digit = (n & 0xf000) >> 12;
527
528 this.add(hex[digit]);
529 n <<= 4;
530 }
531 }
532
533 public void setIgnoreHierarchy(boolean ignoreHierarchy) {
534 this.ignoreHierarchy = ignoreHierarchy;
535 }
536
537 /***
538 * If true, an Enum is serialized as a bean with a special property
539 * _name=name() as all as all other properties defined within the enum.<br/>
540 * If false, an Enum is serialized as a name=value pair (name=name())
541 *
542 * @param enumAsBean
543 * true to serialize an enum as a bean instead of as a name=value
544 * pair (default=false)
545 */
546 public void setEnumAsBean(boolean enumAsBean) {
547 this.enumAsBean = enumAsBean;
548 }
549 }