JodaBeanJsonWriter.java
/*
* Copyright 2001-present Stephen Colebourne
*
* Licensed 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.joda.beans.ser.json;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.joda.beans.Bean;
import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaProperty;
import org.joda.beans.ser.JodaBeanSer;
import org.joda.beans.ser.SerCategory;
import org.joda.beans.ser.SerIterator;
import org.joda.beans.ser.SerOptional;
import org.joda.beans.ser.SerTypeMapper;
import org.joda.convert.StringConverter;
/**
* Provides the ability for a Joda-Bean to be written to JSON.
* <p>
* This class contains mutable state and cannot be used from multiple threads.
* A new instance must be created for each message.
* <p>
* The JSON format is kept relatively natural, however some meta-data is added.
* This has the unfortunate effect of adding an additional object structure to
* hold the type in a few places.
* <p>
* Beans are output using JSON objects where the key is the property name.
* The type of the bean will be sent using the '@type' property name if necessary.
* <p>
* Most simple types, defined by Joda-Convert, are output as JSON strings.
* If the simple type requires additional type information, the value is replaced by
* a JSON object containing the keys '@type' and 'value'.
* <p>
* Null values are generally omitted, but where included are sent as 'null'.
* Boolean values are sent as 'true' and 'false'.
* Integer and Double values are sent as JSON numbers.
* Other numeric types are also sent as numbers but may have additional type information.
* <p>
* Collections are output using JSON objects or arrays.
* Multisets are output as a map of value to count.
* <p>
* If a collection contains a collection then addition meta-type information is
* written to aid with deserialization.
* At this level, the data read back may not be identical to that written.
* If the collection type requires additional type information, the value is replaced by
* a JSON object containing the keys '@meta' and 'value'.
* <p>
* Type names are shortened by the package of the root type if possible.
* Certain basic types are also handled, such as String, Integer, File and URI.
*/
public class JodaBeanJsonWriter {
/**
* JSON bean type attribute.
*/
static final String BEAN = "@bean";
/**
* JSON simple type attribute.
*/
static final String TYPE = "@type";
/**
* JSON meta-type attribute.
*/
static final String META = "@meta";
/**
* JSON value attribute.
*/
static final String VALUE = "value";
/**
* The settings to use.
*/
private final JodaBeanSer settings;
/**
* The outputter.
*/
private JsonOutput output;
/**
* The base package including the trailing dot.
*/
private String basePackage;
/**
* The known types.
*/
private Map<Class<?>, String> knownTypes = new HashMap<>();
/**
* Creates an instance.
*
* @param settings the settings to use, not null
*/
public JodaBeanJsonWriter(final JodaBeanSer settings) {
JodaBeanUtils.notNull(settings, "settings");
this.settings = settings;
}
//-----------------------------------------------------------------------
/**
* Writes the bean to a string.
* <p>
* The type of the bean will be set in the message.
*
* @param bean the bean to output, not null
* @return the JSON, not null
*/
public String write(Bean bean) {
return write(bean, true);
}
/**
* Writes the bean to a string specifying whether to include the type at the root.
*
* @param bean the bean to output, not null
* @param rootType true to output the root type
* @return the JSON, not null
*/
public String write(Bean bean, boolean rootType) {
StringBuilder buf = new StringBuilder(1024);
try {
write(bean, rootType, buf);
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
return buf.toString();
}
/**
* Writes the bean to the {@code Appendable}.
* <p>
* The type of the bean will be set in the message.
*
* @param bean the bean to output, not null
* @param output the output appendable, not null
* @throws IOException if an error occurs
*/
public void write(Bean bean, Appendable output) throws IOException {
write(bean, true, output);
}
/**
* Writes the bean to the {@code Appendable} specifying whether to include the type at the root.
*
* @param bean the bean to output, not null
* @param rootType true to output the root type
* @param output the output appendable, not null
* @throws IOException if an error occurs
*/
public void write(Bean bean, boolean rootType, Appendable output) throws IOException {
JodaBeanUtils.notNull(bean, "bean");
JodaBeanUtils.notNull(output, "output");
this.output = new JsonOutput(output, settings.getIndent(), settings.getNewLine());
writeBean(bean, bean.getClass(), rootType ? RootType.ROOT_WITH_TYPE : RootType.ROOT_WITHOUT_TYPE);
output.append(settings.getNewLine());
}
//-----------------------------------------------------------------------
// write a bean as a JSON object
private void writeBean(Bean bean, Class<?> declaredType, RootType rootTypeFlag) throws IOException {
output.writeObjectStart();
// type information
if (rootTypeFlag == RootType.ROOT_WITH_TYPE || (rootTypeFlag == RootType.NOT_ROOT && bean.getClass() != declaredType)) {
String typeStr = SerTypeMapper.encodeType(bean.getClass(), settings, basePackage, knownTypes);
if (rootTypeFlag == RootType.ROOT_WITH_TYPE) {
basePackage = bean.getClass().getPackage().getName() + ".";
}
output.writeObjectKeyValue(BEAN, typeStr);
}
// property information
for (MetaProperty<?> prop : bean.metaBean().metaPropertyIterable()) {
if (prop.style().isSerializable() || (prop.style().isDerived() && settings.isIncludeDerived())) {
Object value = SerOptional.extractValue(prop, bean);
if (value != null) {
output.writeObjectKey(prop.name());
Class<?> propType = SerOptional.extractType(prop, bean.getClass());
if (value instanceof Bean) {
if (settings.getConverter().isConvertible(value.getClass())) {
writeSimple(propType, value);
} else {
writeBean((Bean) value, propType, RootType.NOT_ROOT);
}
} else {
SerIterator itemIterator = settings.getIteratorFactory().create(value, prop, bean.getClass());
if (itemIterator != null) {
writeElements(itemIterator);
} else {
writeSimple(propType, value);
}
}
}
}
}
output.writeObjectEnd();
}
//-----------------------------------------------------------------------
// write a collection
private void writeElements(SerIterator itemIterator) throws IOException {
if (itemIterator.metaTypeRequired()) {
output.writeObjectStart();
output.writeObjectKeyValue(META, itemIterator.metaTypeName());
output.writeObjectKey(VALUE);
}
if (itemIterator.category() == SerCategory.MAP) {
writeMap(itemIterator);
} else if (itemIterator.category() == SerCategory.COUNTED) {
writeCounted(itemIterator);
} else if (itemIterator.category() == SerCategory.TABLE) {
writeTable(itemIterator);
} else if (itemIterator.category() == SerCategory.GRID) {
writeGrid(itemIterator);
} else {
writeArray(itemIterator);
}
if (itemIterator.metaTypeRequired()) {
output.writeObjectEnd();
}
}
// write list/set/array
private void writeArray(SerIterator itemIterator) throws IOException {
output.writeArrayStart();
while (itemIterator.hasNext()) {
itemIterator.next();
output.writeArrayItemStart();
writeObject(itemIterator.valueType(), itemIterator.value(), itemIterator);
}
output.writeArrayEnd();
}
// write map
private void writeMap(SerIterator itemIterator) throws IOException {
// if key type is known and convertible use short key format, else use full bean format
if (settings.getConverter().isConvertible(itemIterator.keyType())) {
writeMapSimple(itemIterator);
} else {
writeMapComplex(itemIterator);
}
}
// write map with simple keys
private void writeMapSimple(SerIterator itemIterator) throws IOException {
StringConverter<Object> keyConverter = settings.getConverter().findConverterNoGenerics(itemIterator.keyType());
output.writeObjectStart();
while (itemIterator.hasNext()) {
itemIterator.next();
Object key = itemIterator.key();
if (key == null) {
throw new IllegalArgumentException("Unable to write map key as it cannot be null");
}
String str = keyConverter.convertToString(itemIterator.key());
if (str == null) {
throw new IllegalArgumentException("Unable to write map key as it cannot be a null string");
}
output.writeObjectKey(str);
writeObject(itemIterator.valueType(), itemIterator.value(), itemIterator);
}
output.writeObjectEnd();
}
// write map with complex keys
private void writeMapComplex(SerIterator itemIterator) throws IOException {
output.writeArrayStart();
while (itemIterator.hasNext()) {
itemIterator.next();
Object key = itemIterator.key();
if (key == null) {
throw new IllegalArgumentException("Unable to write map key as it cannot be null: " + key);
}
output.writeArrayItemStart();
output.writeArrayStart();
output.writeArrayItemStart();
writeObject(itemIterator.keyType(), key, null);
output.writeArrayItemStart();
writeObject(itemIterator.valueType(), itemIterator.value(), itemIterator);
output.writeArrayEnd();
}
output.writeArrayEnd();
}
// write table
private void writeTable(SerIterator itemIterator) throws IOException {
output.writeArrayStart();
while (itemIterator.hasNext()) {
itemIterator.next();
output.writeArrayItemStart();
output.writeArrayStart();
output.writeArrayItemStart();
writeObject(itemIterator.keyType(), itemIterator.key(), null);
output.writeArrayItemStart();
writeObject(itemIterator.columnType(), itemIterator.column(), null);
output.writeArrayItemStart();
writeObject(itemIterator.valueType(), itemIterator.value(), itemIterator);
output.writeArrayEnd();
}
output.writeArrayEnd();
}
// write grid using sparse approach
private void writeGrid(SerIterator itemIterator) throws IOException {
output.writeArrayStart();
output.writeArrayItemStart();
output.writeInt(itemIterator.dimensionSize(0));
output.writeArrayItemStart();
output.writeInt(itemIterator.dimensionSize(1));
while (itemIterator.hasNext()) {
itemIterator.next();
output.writeArrayItemStart();
output.writeArrayStart();
output.writeArrayItemStart();
output.writeInt((Integer) itemIterator.key());
output.writeArrayItemStart();
output.writeInt((Integer) itemIterator.column());
output.writeArrayItemStart();
writeObject(itemIterator.valueType(), itemIterator.value(), itemIterator);
output.writeArrayEnd();
}
output.writeArrayEnd();
}
// write counted set
private void writeCounted(final SerIterator itemIterator) throws IOException {
output.writeArrayStart();
while (itemIterator.hasNext()) {
itemIterator.next();
output.writeArrayItemStart();
output.writeArrayStart();
output.writeArrayItemStart();
writeObject(itemIterator.valueType(), itemIterator.value(), itemIterator);
output.writeArrayItemStart();
output.writeInt(itemIterator.count());
output.writeArrayEnd();
}
output.writeArrayEnd();
}
// write collection object
private void writeObject(Class<?> declaredType, Object obj, SerIterator parentIterator) throws IOException {
if (obj == null) {
output.writeNull();
} else if (settings.getConverter().isConvertible(obj.getClass())) {
writeSimple(declaredType, obj);
} else if (obj instanceof Bean) {
writeBean((Bean) obj, declaredType, RootType.NOT_ROOT);
} else if (parentIterator != null) {
SerIterator childIterator = settings.getIteratorFactory().createChild(obj, parentIterator);
if (childIterator != null) {
writeElements(childIterator);
} else {
writeSimple(declaredType, obj);
}
} else {
writeSimple(declaredType, obj);
}
}
//-----------------------------------------------------------------------
// write simple type
private void writeSimple(Class<?> declaredType, Object value) throws IOException {
// simple types have no need to write a type object
Class<?> realType = value.getClass();
if (realType == Integer.class) {
output.writeInt(((Integer) value).intValue());
return;
} else if (realType == Double.class) {
double dbl = ((Double) value).doubleValue();
if (Double.isNaN(dbl) == false && Double.isInfinite(dbl) == false) {
output.writeDouble(dbl);
return;
}
} else if (realType == Boolean.class) {
output.writeBoolean(((Boolean) value).booleanValue());
return;
}
// handle no declared type and subclasses
Class<?> effectiveType = declaredType;
boolean requiresClose = false;
if (declaredType == Object.class) {
if (realType != String.class) {
effectiveType = settings.getConverter().findTypedConverter(realType).getEffectiveType();
String typeStr = SerTypeMapper.encodeType(effectiveType, settings, basePackage, knownTypes);
output.writeObjectStart();
output.writeObjectKeyValue(TYPE, typeStr);
output.writeObjectKey(VALUE);
requiresClose = true;
} else {
effectiveType = realType;
}
} else if (settings.getConverter().isConvertible(declaredType) == false) {
effectiveType = settings.getConverter().findTypedConverter(realType).getEffectiveType();
String typeStr = SerTypeMapper.encodeType(effectiveType, settings, basePackage, knownTypes);
output.writeObjectStart();
output.writeObjectKeyValue(TYPE, typeStr);
output.writeObjectKey(VALUE);
requiresClose = true;
}
// long/short/byte/float only processed now to ensure that exact numeric type can be identified
if (realType == Long.class) {
output.writeLong(((Long) value).longValue());
} else if (realType == Short.class) {
output.writeInt(((Short) value).shortValue());
} else if (realType == Byte.class) {
output.writeInt(((Byte) value).byteValue());
} else if (realType == Float.class) {
output.writeFloat(((Float) value).floatValue());
} else {
// write as a string
try {
String converted = settings.getConverter().convertToString(effectiveType, value);
if (converted == null) {
throw new IllegalArgumentException("Unable to write because converter returned a null string: " + value);
}
output.writeString(converted);
} catch (RuntimeException ex) {
throw new IllegalArgumentException(
"Unable to convert type " + effectiveType.getName() + " declared as " + declaredType.getName(), ex);
}
}
// close open map
if (requiresClose) {
output.writeObjectEnd();
}
}
//-----------------------------------------------------------------------
enum RootType {
ROOT_WITH_TYPE,
ROOT_WITHOUT_TYPE,
NOT_ROOT,
}
}