All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.shindig.common.JsonSerializer Maven / Gradle / Ivy

The newest version!
 /*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.shindig.common;

import org.apache.shindig.common.util.DateUtil;
import org.apache.shindig.common.uri.Uri;
import org.joda.time.DateTime;
import org.json.JSONArray;
import org.json.JSONObject;

import com.google.common.collect.Multimap;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;

/**
 * Serializes a JSONObject.
 *
 * The methods here are designed to be substantially more CPU and memory efficient than those found
 * in org.json or net.sf.json. In profiling, the performance of both of these libraries has been
 * found to be woefully inadequate for large scale deployments.
 *
 * The append*() methods can be used to serialize directly into an Appendable, such as an output
 * stream. This avoids unnecessary copies to intermediate objects.
 *
 * To reduce output size, null values in json arrays and objects will always be removed.
 */
public final class JsonSerializer {
  // Multiplier to use for allocating the buffer.
  private static final int BASE_MULTIPLIER = 256;

  private static final char[] HEX_DIGITS = {
    '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
  };

  private JsonSerializer() {}

  public static String serialize(Object object) {
    StringBuilder buf = new StringBuilder(1024);
    try {
      append(buf, object);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    return buf.toString();
  }

  /**
   * Serialize a JSONObject. Does not guard against cyclical references.
   */
  public static String serialize(JSONObject object) {
    StringBuilder buf = new StringBuilder(object.length() * BASE_MULTIPLIER);
    try {
      appendJsonObject(buf, object);
    } catch (IOException e) {
      // Shouldn't ever happen unless someone adds something to append*.
      throw new RuntimeException(e);
    }
    return buf.toString();
  }

  /**
   * Serializes a Map as a JSON object. Does not guard against cyclical references.
   */
  public static String serialize(Map map) {
    StringBuilder buf = new StringBuilder(map.size() * BASE_MULTIPLIER);
    try {
      appendMap(buf, map);
    } catch (IOException e) {
      // Shouldn't ever happen unless someone adds something to append*.
      throw new RuntimeException(e);
    }
    return buf.toString();
  }

  /**
   * Serializes a Collection as a JSON array. Does not guard against cyclical references.
   */
  public static String serialize(Collection collection) {
    StringBuilder buf = new StringBuilder(collection.size() * BASE_MULTIPLIER);
    try {
      appendCollection(buf, collection);
    } catch (IOException e) {
      // Shouldn't ever happen unless someone adds something to append*.
      throw new RuntimeException(e);
    }
    return buf.toString();
  }

  /**
   * Serializes an array as a JSON array. Does not guard against cyclical references
   */
  public static String serialize(Object[] array) {
    StringBuilder buf = new StringBuilder(array.length * BASE_MULTIPLIER);
    try {
      appendArray(buf, array);
    } catch (IOException e) {
      // Shouldn't ever happen unless someone adds something to append*.
      throw new RuntimeException(e);
    }
    return buf.toString();
  }

  /**
   * Serializes a JSON array. Does not guard against cyclical references
   */
  public static String serialize(JSONArray array) {
    StringBuilder buf = new StringBuilder(array.length() * BASE_MULTIPLIER);
    try {
      appendJsonArray(buf, array);
    } catch (IOException e) {
      // Shouldn't ever happen unless someone adds something to append*.
      throw new RuntimeException(e);
    }
    return buf.toString();
  }

  /**
   * Appends a value to the buffer.
   *
   * @throws IOException If {@link Appendable#append(char)} throws an exception.
   */
  @SuppressWarnings("unchecked")
  public static void append(Appendable buf, Object value) throws IOException {
    if (value == null || value == JSONObject.NULL) {
      buf.append("null");
    } else if (value instanceof Number ||
               value instanceof Boolean) {
      // Primitives
      buf.append(value.toString());
    } else if (value instanceof CharSequence ||
               value instanceof DateTime ||
               value instanceof Locale ||
               value instanceof Uri ||
               value.getClass().isEnum()) {
      // String-like Primitives
      appendString(buf, value.toString());
    } else if (value instanceof Date) {
      appendString(buf, DateUtil.formatIso8601Date((Date)value));
    } else if (value instanceof JSONObject) {
      appendJsonObject(buf, (JSONObject) value);
    } else if (value instanceof JSONArray) {
      appendJsonArray(buf, (JSONArray) value);
    } else if (value instanceof Map) {
      appendMap(buf, (Map) value);
    } else if (value instanceof Multimap) {
      appendMultimap(buf, (Multimap) value);
    } else if (value instanceof Collection) {
      appendCollection(buf, (Collection) value);
    } else if (value.getClass().isArray()) {
      appendArray(buf, (Object[]) value);
    } else {
      // Try getter conversion
      appendPojo(buf, value);
    }
  }

  /**
   * Appends a java object using getters
   *
   * @throws IOException If {@link Appendable#append(char)} throws an exception.
   */
  public static void appendPojo(Appendable buf, Object pojo) throws IOException {
    Map methods = JsonUtil.getGetters(pojo);
    buf.append('{');
    boolean firstDone = false;
    for (Map.Entry entry : methods.entrySet()) {
      try {
        Object value = entry.getValue().invoke(pojo);
        if (value != null) {
          String attribute = entry.getKey();

          // Common use case isOwner/isViewer should not be set unless true
          if (!(("isOwner".equals(attribute) || "isViewer".equals(attribute)) && value.equals(Boolean.FALSE))) {
            // Drop null values.
            if (firstDone) {
              buf.append(',');
            } else {
              firstDone = true;
            }
            appendString(buf, attribute);
            buf.append(':');
            append(buf, value);
          }
        }
      } catch (IllegalArgumentException e) {
        // Shouldn't be possible.
        throw new RuntimeException(e);
      } catch (IllegalAccessException e) {
        // Bad class.
        throw new RuntimeException(e);
      } catch (InvocationTargetException e) {
        // Bad class.
        throw new RuntimeException(e);
      }
    }
    buf.append('}');
  }

  /**
   * Appends an array to the buffer.
   *
   * @throws IOException If {@link Appendable#append(char)} throws an exception.
   */
  public static void appendArray(Appendable buf, Object[] array) throws IOException {
    buf.append('[');
    boolean firstDone = false;
    for (Object o : array) {
      if (o != null) {
        if (firstDone) {
          buf.append(',');
        } else {
          firstDone = true;
        }
        append(buf, o);
      }
    }
    buf.append(']');
  }

  /**
   * Append a JSONArray to the buffer.
   * @throws IOException If {@link Appendable#append(char)} throws an exception.
   */
  public static void appendJsonArray(Appendable buf, JSONArray array) throws IOException {
    buf.append('[');
    boolean firstDone = false;
    for (int i = 0, j = array.length(); i < j; ++i) {
      Object value = array.opt(i);
      if (value != null) {
        if (firstDone) {
          buf.append(',');
        } else {
          firstDone = true;
        }
        append(buf, value);
      }
    }
    buf.append(']');
  }

  /**
   * Appends a Collection to the buffer.
   *
   * @throws IOException If {@link Appendable#append(char)} throws an exception.
   */
  public static void appendCollection(Appendable buf, Collection collection)
      throws IOException {
    buf.append('[');
    boolean firstDone = false;
    for (Object o : collection) {
      if (o != null) {
        if (firstDone) {
          buf.append(',');
        } else {
          firstDone = true;
        }
        append(buf, o);
      }
    }
    buf.append(']');
  }

  /**
   * Appends a Map to the buffer.
   *
   * @throws IOException If {@link Appendable#append(char)} throws an exception.
   */
  public static void appendMap(final Appendable buf, final Map map) throws IOException {
    buf.append('{');
    boolean firstDone = false;
    for (Map.Entry entry : map.entrySet()) {
      Object value = entry.getValue();
      if (value != null) {
        if (firstDone) {
          buf.append(',');
        } else {
          firstDone = true;
        }
        Object key = entry.getKey();

        appendString(buf, key.toString());
        buf.append(':');
        append(buf, value);
      }
    }
    buf.append('}');
  }

  /**
   * Appends a Map to the buffer.
   *
   * @throws IOException If {@link Appendable#append(char)} throws an exception.
   */
  public static void appendMultimap(Appendable buf, Multimap map) throws IOException {
    appendMap(buf, map.asMap());
  }
  
  /**
   * Appends a JSONObject to the buffer.
   *
   * @throws IOException If {@link Appendable#append(char)} throws an exception.
   */
  @SuppressWarnings("unchecked")
  public static void appendJsonObject(Appendable buf, JSONObject object) throws IOException {
    buf.append('{');
    Iterator keys = object.keys();
    boolean firstDone = false;
    while (keys.hasNext()) {
      String key = keys.next();
      Object value = object.opt(key);
      if (value != null) {
        if (firstDone) {
          buf.append(',');
        } else {
          firstDone = true;
        }
        appendString(buf, key);
        buf.append(':');
        append(buf, value);
      }
    }
    buf.append('}');
  }

  /**
   * Appends a string to the buffer. The string will be JSON encoded and enclosed in quotes.
   *
   * @throws IOException If {@link Appendable#append(char)} throws an exception.
   */
  public static void appendString(Appendable buf, CharSequence string) throws IOException {
    if (string == null || string.length() == 0) {
      buf.append("\"\"");
      return;
    }

    char current = 0;
    buf.append('"');
    for (int i = 0, j = string.length(); i < j; ++i) {
      current = string.charAt(i);
      switch (current) {
        case '\\':
        case '"':
          buf.append('\\');
          buf.append(current);
          break;
        // We escape angle brackets in order to prevent content sniffing in user agents like IE.
        // This content sniffing can potentially be used to bypass other security restrictions.
        case '<':
          buf.append("\\u003c");
          break;
        case '>':
          buf.append("\\u003e");
          break;
        default:
          if (current < ' ' || (current >= '\u0080' && current < '\u00a0') ||
              (current >= '\u2000' && current < '\u2100')) {
            buf.append('\\');
            switch (current) {
              case '\b':
                buf.append('b');
                break;
              case '\t':
                buf.append('t');
                break;
              case '\n':
                buf.append('n');
                break;
              case '\f':
                buf.append('f');
                break;
              case '\r':
                buf.append('r');
                break;
              default:
                // The possible alternative approaches for dealing with unicode characters are
                // as follows:
                // Method 1 (from json.org.JSONObject)
                // 1. Append "000" + Integer.toHexString(current)
                // 2. Truncate this value to 4 digits by using value.substring(value.length() - 4)
                //
                // Method 2 (from net.sf.json.JSONObject)
                // This method is fairly unique because the entire thing uses an intermediate fixed
                // size buffer of 1KB. It's an interesting approach, but overall performs worse than
                // org.json
                // 1. Append "000" + Integer.toHexString(current)
                // 2. Append value.charAt(value.length() - 4)
                // 2. Append value.charAt(value.length() - 3)
                // 2. Append value.charAt(value.length() - 2)
                // 2. Append value.charAt(value.length() - 1)
                //
                // Method 3 (previous experiment)
                // 1. Calculate Integer.hexString(current)
                // 2. for (int i = 0; i < 4 - value.length(); ++i) { buf.append('0'); }
                // 3. buf.append(value)
                //
                // Method 4 (Sun conversion from java.util.Properties)
                // 1. Append '\'
                // 2. Append 'u'
                // 3. Append each of 4 octets by indexing into a hex array.
                //
                // Method 5
                // Index into a single lookup table of all relevant lookup values.
                buf.append('u');
                buf.append(HEX_DIGITS[(current >> 12) & 0xF]);
                buf.append(HEX_DIGITS[(current >>  8) & 0xF]);
                buf.append(HEX_DIGITS[(current >>  4) & 0xF]);
                buf.append(HEX_DIGITS[current & 0xF]);
             }
          } else {
            buf.append(current);
          }
      }
    }
    buf.append('"');
  }
}