/*
* 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<String, ?> 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<String, Object>) value);
} else if (value instanceof Multimap) {
appendMultimap(buf, (Multimap<String, Object>) value);
} else if (value instanceof Collection) {
appendCollection(buf, (Collection<Object>) 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<String, Method> methods = JsonUtil.getGetters(pojo);
buf.append('{');
boolean firstDone = false;
for (Map.Entry<String, Method> 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<String, ?> map) throws IOException {
buf.append('{');
boolean firstDone = false;
for (Map.Entry<String, ?> 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<String, Object> 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<String> 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('"');
}
}