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

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