View Javadoc

1   /*
2    * $Id: JSONWriter.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.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             // cyclic reference
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                     // ignore "class" and others
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             // special-case handling for an Enumeration - include the name() as
237             // a property */
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; // one report per map
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 }