blob: 46012af6cefa06018144d0f3dc12c7ca5aa1f1cb [file] [log] [blame]
/*
* 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.pivot.json;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import org.apache.pivot.beans.BeanAdapter;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.HashMap;
import org.apache.pivot.collections.List;
import org.apache.pivot.collections.Map;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.io.EchoReader;
import org.apache.pivot.io.EchoWriter;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.serialization.Serializer;
/**
* Implementation of the {@link Serializer} interface that reads data from
* and writes data to a JavaScript Object Notation (JSON) file.
*/
public class JSONSerializer implements Serializer<Object> {
private Charset charset;
private Class<?> type;
private int c = -1;
private boolean alwaysDelimitMapKeys = false;
private boolean verbose = false;
public static final String DEFAULT_CHARSET_NAME = "UTF-8";
public static final String MIME_TYPE = "application/json";
public static final int BUFFER_SIZE = 2048;
public JSONSerializer() {
this(Charset.forName(DEFAULT_CHARSET_NAME), Object.class);
}
public JSONSerializer(Charset charset) {
this(charset, Object.class);
}
public JSONSerializer(Class<?> type) {
this(Charset.forName(DEFAULT_CHARSET_NAME), type);
}
public JSONSerializer(Charset charset, Class<?> type) {
if (charset == null) {
throw new IllegalArgumentException("charset is null.");
}
this.charset = charset;
this.type = type;
}
/**
* Returns the character set used to encode/decode the JSON data.
*/
public Charset getCharset() {
return charset;
}
/**
* Returns the type of the object that will be returned by {@link #readObject(Reader)}.
*/
public Class<?> getType() {
return type;
}
/**
* Returns a flag indicating whether or not map keys will always be
* quote-delimited.
*/
public boolean getAlwaysDelimitMapKeys() {
return alwaysDelimitMapKeys;
}
/**
* Sets a flag indicating that map keys should always be quote-delimited.
*
* @param alwaysDelimitMapKeys
* <tt>true</tt> to bound map keys in double quotes; <tt>false</tt> to
* only quote-delimit keys as necessary.
*/
public void setAlwaysDelimitMapKeys(boolean alwaysDelimitMapKeys) {
this.alwaysDelimitMapKeys = alwaysDelimitMapKeys;
}
/**
* Returns the serializer's verbosity flag.
*/
public boolean isVerbose() {
return verbose;
}
/**
* Sets the serializer's verbosity flag. When verbosity is enabled, all data read or
* written will be echoed to the console.
*
* @param verbose
*/
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
/**
* Reads data from a JSON stream.
*
* @param inputStream
* The input stream from which data will be read.
*
* @see #readObject(Reader)
*/
@Override
public Object readObject(InputStream inputStream)
throws IOException, SerializationException {
if (inputStream == null) {
throw new IllegalArgumentException("inputStream is null.");
}
Reader reader = new BufferedReader(new InputStreamReader(inputStream, charset), BUFFER_SIZE);
if (verbose) {
reader = new EchoReader(reader);
}
return readObject(reader);
}
/**
* Reads data from a JSON stream.
*
* @param reader
* The reader from which data will be read.
*
* @return
* One of the following types, depending on the content of the stream:
*
* <ul>
* <li>java.lang.String</li>
* <li>java.lang.Number</li>
* <li>java.lang.Boolean</li>
* <li>pivot.collections.List</li>
* <li>pivot.collections.Map</li>
* </ul>
*/
public Object readObject(Reader reader)
throws IOException, SerializationException {
if (reader == null) {
throw new IllegalArgumentException("reader is null.");
}
// Move to the first character
c = reader.read();
// Ignore BOM (if present)
if (c == 0xFEFF) {
c = reader.read();
}
// Read the root value
LineNumberReader lineNumberReader = new LineNumberReader(reader);
Object object;
try {
object = readValue(lineNumberReader, type);
} catch (SerializationException exception) {
System.err.println("An error occurred while processing input at line number "
+ (lineNumberReader.getLineNumber() + 1));
throw exception;
}
return object;
}
private Object readValue(Reader reader, Type type)
throws IOException, SerializationException {
Object object = null;
skipWhitespaceAndComments(reader);
if (c == -1) {
throw new SerializationException("Unexpected end of input stream.");
}
if (c == 'n') {
object = readNull(reader);
} else if (c == '"' || c == '\'') {
object = readString(reader, type);
} else if (c == '+' || c == '-' || Character.isDigit(c)) {
object = readNumber(reader, type);
} else if (c == 't' || c == 'f') {
object = readBoolean(reader, type);
} else if (c == '[') {
object = readList(reader, type);
} else if (c == '{') {
object = readMap(reader, type);
} else {
throw new SerializationException("Unexpected character in input stream.");
}
return object;
}
private void skipWhitespaceAndComments(Reader reader)
throws IOException, SerializationException {
while (c != -1
&& (Character.isWhitespace(c)
|| c == '/')) {
boolean comment = (c == '/');
// Read the next character
c = reader.read();
if (comment) {
if (c == '/') {
// Single-line comment
while (c != -1
&& c != '\n'
&& c != '\r') {
c = reader.read();
}
} else if (c == '*') {
// Multi-line comment
boolean closed = false;
while (c != -1
&& !closed) {
c = reader.read();
if (c == '*') {
c = reader.read();
closed = (c == '/');
}
}
if (!closed) {
throw new SerializationException("Unexpected end of input stream.");
}
if (c != -1) {
c = reader.read();
}
} else {
throw new SerializationException("Unexpected character in input stream.");
}
}
}
}
private Object readNull(Reader reader)
throws IOException, SerializationException {
String nullString = "null";
int n = nullString.length();
int i = 0;
while (c != -1 && i < n) {
if (nullString.charAt(i) != c) {
throw new SerializationException("Unexpected character in input stream.");
}
c = reader.read();
i++;
}
if (i < n) {
throw new SerializationException("Incomplete null value in input stream.");
}
return null;
}
private Object readString(Reader reader, Type type)
throws IOException, SerializationException {
if (!(type instanceof Class<?>)) {
throw new SerializationException("Cannot convert string to " + type + ".");
}
StringBuilder stringBuilder = new StringBuilder();
// Use the same delimiter to close the string
int t = c;
// Move to the next character after the delimiter
c = reader.read();
while (c != -1 && c != t) {
if (!Character.isISOControl(c)) {
if (c == '\\') {
c = reader.read();
if (c == 'b') {
c = '\b';
} else if (c == 'f') {
c = '\f';
} else if (c == 'n') {
c = '\n';
} else if (c == 'r') {
c = '\r';
} else if (c == 't') {
c = '\t';
} else if (c == 'u') {
StringBuilder unicodeBuilder = new StringBuilder();
while (unicodeBuilder.length() < 4) {
c = reader.read();
unicodeBuilder.append((char)c);
}
String unicode = unicodeBuilder.toString();
c = (char)Integer.parseInt(unicode, 16);
} else {
if (!(c == '\\'
|| c == '/'
|| c == '\"'
|| c == '\''
|| c == t)) {
throw new SerializationException("Unsupported escape sequence in input stream.");
}
}
}
stringBuilder.append((char)c);
}
c = reader.read();
}
if (c != t) {
throw new SerializationException("Unterminated string in input stream.");
}
// Move to the next character after the delimiter
c = reader.read();
return BeanAdapter.coerce(stringBuilder.toString(), (Class<?>)type);
}
private Object readNumber(Reader reader, Type type)
throws IOException, SerializationException {
if (!(type instanceof Class<?>)) {
throw new SerializationException("Cannot convert number to " + type + ".");
}
Number number = null;
StringBuilder stringBuilder = new StringBuilder();
boolean negative = false;
boolean integer = true;
if (c == '+' || c == '-') {
negative = (c == '-');
c = reader.read();
}
while (c != -1 && (Character.isDigit(c) || c == '.'
|| c == 'e' || c == 'E' || c == '-')) {
stringBuilder.append((char)c);
integer &= !(c == '.');
c = reader.read();
}
if (integer) {
// TODO 5/28/2008 Remove 32-bit optimization when 64-bit processors
// are more prevalent
long value = Long.parseLong(stringBuilder.toString()) * (negative ? -1 : 1);
if (value > Integer.MAX_VALUE
|| value < Integer.MIN_VALUE) {
number = value;
} else {
number = (int)value;
}
} else {
number = Double.parseDouble(stringBuilder.toString()) * (negative ? -1.0d : 1.0d);
}
return BeanAdapter.coerce(number, (Class<?>)type);
}
private Object readBoolean(Reader reader, Type type)
throws IOException, SerializationException {
if (!(type instanceof Class<?>)) {
throw new SerializationException("Cannot convert number to " + type + ".");
}
String text = (c == 't') ? "true" : "false";
int n = text.length();
int i = 0;
while (c != -1 && i < n) {
if (text.charAt(i) != c) {
throw new SerializationException("Unexpected character in input stream.");
}
c = reader.read();
i++;
}
if (i < n) {
throw new SerializationException("Incomplete boolean value in input stream.");
}
return BeanAdapter.coerce(Boolean.parseBoolean(text), (Class<?>)type);
}
@SuppressWarnings("unchecked")
private Object readList(Reader reader, Type type)
throws IOException, SerializationException {
Sequence<Object> sequence;
Type itemType;
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType)type;
Class<?> rawType = (Class<?>)parameterizedType.getRawType();
if (!Sequence.class.isAssignableFrom(rawType)) {
throw new IllegalArgumentException("Cannot convert array to "
+ rawType.getName() + ".");
}
try {
sequence = (Sequence<Object>)rawType.newInstance();
} catch (InstantiationException exception) {
throw new RuntimeException(exception);
} catch (IllegalAccessException exception) {
throw new RuntimeException(exception);
}
// Get the target item type
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
itemType = actualTypeArguments[0];
} else if (type == Object.class) {
sequence = new ArrayList<Object>();
itemType = Object.class;
} else {
throw new IllegalArgumentException("Cannot convert array to " + type + ".");
}
// Move to the next character after '['
c = reader.read();
skipWhitespaceAndComments(reader);
while (c != -1 && c != ']') {
sequence.add(readValue(reader, itemType));
skipWhitespaceAndComments(reader);
if (c == ',') {
c = reader.read();
skipWhitespaceAndComments(reader);
} else if (c == -1) {
throw new SerializationException("Unexpected end of input stream.");
} else {
if (c != ']') {
throw new SerializationException("Unexpected character in input stream.");
}
}
}
// Move to the next character after ']'
c = reader.read();
return sequence;
}
@SuppressWarnings("unchecked")
private Object readMap(Reader reader, Type type)
throws IOException, SerializationException {
Dictionary<String, Object> dictionary;
Type valueType;
if (type instanceof ParameterizedType) {
// Instantiate the target dictionary
ParameterizedType parameterizedType = (ParameterizedType)type;
Class<?> rawType = (Class<?>)parameterizedType.getRawType();
if (!Dictionary.class.isAssignableFrom(rawType)) {
throw new IllegalArgumentException("Cannot convert object to "
+ rawType.getName() + ".");
}
try {
dictionary = (Dictionary<String, Object>)rawType.newInstance();
} catch (InstantiationException exception) {
throw new RuntimeException(exception);
} catch (IllegalAccessException exception) {
throw new RuntimeException(exception);
}
// Get the target value type
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
valueType = actualTypeArguments[1];
} else if (type == Object.class){
dictionary = new HashMap<String, Object>();
valueType = Object.class;
} else {
Class<?> beanType = (Class<?>)type;
try {
dictionary = new BeanAdapter(beanType.newInstance());
} catch (InstantiationException exception) {
throw new RuntimeException(exception);
} catch (IllegalAccessException exception) {
throw new RuntimeException(exception);
}
valueType = null;
}
// Move to the next character after '{'
c = reader.read();
skipWhitespaceAndComments(reader);
while (c != -1 && c != '}') {
String key = null;
if (c == '"' || c == '\'') {
// The key is a delimited string
key = (String)readString(reader, String.class);
} else {
// The key is an undelimited string; it must adhere to Java
// identifier syntax
StringBuilder keyStringBuilder = new StringBuilder();
if (!Character.isJavaIdentifierStart(c)) {
throw new SerializationException("Illegal identifier start character.");
}
while (c != -1
&& c != ':' && !Character.isWhitespace(c)) {
if (!Character.isJavaIdentifierPart(c)) {
throw new SerializationException("Illegal identifier character.");
}
keyStringBuilder.append((char)c);
c = reader.read();
}
if (c == -1) {
throw new SerializationException("Unexpected end of input stream.");
}
key = keyStringBuilder.toString();
}
if (key == null
|| key.length() == 0) {
throw new SerializationException("\"" + key + "\" is not a valid key.");
}
skipWhitespaceAndComments(reader);
if (c != ':') {
throw new SerializationException("Unexpected character in input stream.");
}
// Move to the first character after ':'
c = reader.read();
dictionary.put(key, readValue(reader, (valueType == null) ?
((BeanAdapter)dictionary).getGenericType(key) : valueType));
skipWhitespaceAndComments(reader);
if (c == ',') {
c = reader.read();
skipWhitespaceAndComments(reader);
} else if (c == -1) {
throw new SerializationException("Unexpected end of input stream.");
} else {
if (c != '}') {
throw new SerializationException("Unexpected character in input stream.");
}
}
}
// Move to the first character after '}'
c = reader.read();
return (dictionary instanceof BeanAdapter) ? ((BeanAdapter)dictionary).getBean() : dictionary;
}
/**
* Writes data to a JSON stream.
*
* @param object
*
* @param outputStream
* The output stream to which data will be written.
*
* @see #writeObject(Object, Writer)
*/
@Override
public void writeObject(Object object, OutputStream outputStream)
throws IOException, SerializationException {
if (outputStream == null) {
throw new IllegalArgumentException("outputStream is null.");
}
Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream, charset), BUFFER_SIZE);
if (verbose) {
writer = new EchoWriter(writer);
}
writeObject(object, writer);
}
/**
* Writes data to a JSON stream.
*
* @param object
* The object to serialize. Must be one of the following types:
*
* <ul>
* <li>java.lang.String</li>
* <li>java.lang.Number</li>
* <li>java.lang.Boolean</li>
* <li>pivot.collections.List</li>
* <li>pivot.collections.Map</li>
* </ul>
*
* @param writer
* The writer to which data will be written.
*/
@SuppressWarnings("unchecked")
public void writeObject(Object object, Writer writer)
throws IOException, SerializationException {
if (writer == null) {
throw new IllegalArgumentException("writer is null.");
}
if (object == null) {
writer.append("null");
} else if (object instanceof String) {
String string = (String)object;
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0, n = string.length(); i < n; i++) {
char c = string.charAt(i);
switch(c) {
case '\t': {
stringBuilder.append("\\t");
break;
}
case '\n': {
stringBuilder.append("\\n");
break;
}
case '\\':
case '\"':
case '\'': {
stringBuilder.append("\\" + c);
break;
}
default: {
if (charset.name().startsWith("UTF")
|| c <= 0xFF) {
stringBuilder.append(c);
} else {
stringBuilder.append("\\u");
stringBuilder.append(String.format("%04x", (short)c));
}
}
}
}
writer.append("\"" + stringBuilder.toString() + "\"");
} else if (object instanceof Number) {
Number number = (Number)object;
if (number instanceof Float) {
Float f = (Float)number;
if (f.isNaN()
|| f.isInfinite()) {
throw new SerializationException(number + " is not a valid value.");
}
} else if (number instanceof Double) {
Double d = (Double)number;
if (d.isNaN()
|| d.isInfinite()) {
throw new SerializationException(number + " is not a valid value.");
}
}
writer.append(number.toString());
} else if (object instanceof Boolean) {
writer.append(object.toString());
} else if (object instanceof List<?>) {
List<Object> list = (List<Object>)object;
writer.append("[");
int i = 0;
for (Object item : list) {
if (i > 0) {
writer.append(", ");
}
writeObject(item, writer);
i++;
}
writer.append("]");
} else {
Map<String, Object> map;
if (object instanceof Map<?, ?>) {
map = (Map<String, Object>)object;
} else {
map = new BeanAdapter(object);
}
writer.append("{");
int i = 0;
for (String key : map) {
Object value = map.get(key);
boolean identifier = true;
StringBuilder keyStringBuilder = new StringBuilder();
for (int j = 0, n = key.length(); j < n; j++) {
char c = key.charAt(j);
identifier &= Character.isJavaIdentifierPart(c);
if (c == '"') {
keyStringBuilder.append('\\');
}
keyStringBuilder.append(c);
}
key = keyStringBuilder.toString();
if (i > 0) {
writer.append(", ");
}
// Write the key
if (!identifier || alwaysDelimitMapKeys) {
writer.append('"');
}
writer.append(key);
if (!identifier || alwaysDelimitMapKeys) {
writer.append('"');
}
writer.append(": ");
// Write the value
writeObject(value, writer);
i++;
}
writer.append("}");
}
writer.flush();
}
@Override
public String getMIMEType(Object object) {
return MIME_TYPE + "; charset=" + charset.name();
}
/**
* Converts a JSON value to a Java object.
*
* @param json
* The JSON value.
*
* @return
* The parsed object.
*/
public static Object parse(String json) throws SerializationException {
JSONSerializer jsonSerializer = new JSONSerializer();
Object object;
try {
object = jsonSerializer.readObject(new StringReader(json));
} catch(IOException exception) {
throw new RuntimeException(exception);
}
return object;
}
/**
* Converts a JSON value to a string.
*
* @param json
* The JSON value.
*
* @return
* The parsed string.
*/
public static String parseString(String json) throws SerializationException {
return (String)parse(json);
}
/**
* Converts a JSON value to a number.
*
* @param json
* The JSON value.
*
* @return
* The parsed number.
*/
public static Number parseNumber(String json) throws SerializationException {
return (Number)parse(json);
}
/**
* Converts a JSON value to a short.
*
* @param json
* The JSON value.
*
* @return
* The parsed short.
*/
public static Short parseShort(String json) throws SerializationException {
return (Short)parse(json);
}
/**
* Converts a JSON value to a integer.
*
* @param json
* The JSON value.
*
* @return
* The parsed integer.
*/
public static Integer parseInteger(String json) throws SerializationException {
return (Integer)parse(json);
}
/**
* Converts a JSON value to a long.
*
* @param json
* The JSON value.
*
* @return
* The parsed number.
*/
public static Long parseLong(String json) throws SerializationException {
return (Long)parse(json);
}
/**
* Converts a JSON value to a float.
*
* @param json
* The JSON value.
*
* @return
* The parsed float.
*/
public static Float parseFloat(String json) throws SerializationException {
return (Float)parse(json);
}
/**
* Converts a JSON value to a double.
*
* @param json
* The JSON value.
*
* @return
* The parsed double.
*/
public static Double parseDouble(String json) throws SerializationException {
return (Double)parse(json);
}
/**
* Converts a JSON value to a boolean.
*
* @param json
* The JSON value.
*
* @return
* The parsed boolean.
*/
public static Boolean parseBoolean(String json) throws SerializationException {
return (Boolean)parse(json);
}
/**
* Converts a JSON value to a list.
*
* @param json
* The JSON value.
*
* @return
* The parsed list.
*/
public static List<?> parseList(String json) throws SerializationException {
return (List<?>)parse(json);
}
/**
* Converts a JSON value to a map.
*
* @param json
* The JSON value.
*
* @return
* The parsed map.
*/
@SuppressWarnings("unchecked")
public static Map<String, ?> parseMap(String json) throws SerializationException {
return (Map<String, ?>)parse(json);
}
/**
* Converts a object to a JSON string representation.
*
* @param value
* The object to convert.
*
* @return
* The resulting JSON string.
*/
public static String toString(Object value) throws SerializationException {
JSONSerializer jsonSerializer = new JSONSerializer();
StringWriter writer = new StringWriter();
try {
jsonSerializer.writeObject(value, writer);
} catch(IOException exception) {
throw new RuntimeException(exception);
}
return writer.toString();
}
/**
* Converts a string to a JSON string representation.
*
* @param value
* The object to convert.
*
* @return
* The resulting JSON string.
*/
public static String toString(String value) {
try {
return toString((Object)value);
} catch(SerializationException exception) {
throw new RuntimeException(exception);
}
}
/**
* Converts a number to a JSON string representation.
*
* @param value
* The object to convert.
*
* @return
* The resulting JSON string.
*/
public static String toString(Number value) {
try {
return toString((Object)value);
} catch(SerializationException exception) {
throw new RuntimeException(exception);
}
}
/**
* Converts a boolean to a JSON string representation.
*
* @param value
* The object to convert.
*
* @return
* The resulting JSON string.
*/
public static String toString(Boolean value) {
try {
return toString((Object)value);
} catch(SerializationException exception) {
throw new RuntimeException(exception);
}
}
/**
* Converts a list to a JSON string representation.
*
* @param value
* The object to convert.
*
* @return
* The resulting JSON string.
*/
public static String toString(List<?> value) {
try {
return toString((Object)value);
} catch(SerializationException exception) {
throw new RuntimeException(exception);
}
}
/**
* Converts a map to a JSON string representation.
*
* @param value
* The object to convert.
*
* @return
* The resulting JSON string.
*/
public static String toString(Map<String, ?> value) {
try {
return toString((Object)value);
} catch(SerializationException exception) {
throw new RuntimeException(exception);
}
}
}