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

org.joda.beans.ser.json.JodaBeanJsonWriter Maven / Gradle / Ivy

There is a newer version: 2.11.1
Show newest version
/*
 *  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.
 * 

* This class contains mutable state and cannot be used from multiple threads. * A new instance must be created for each message. *

* 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. *

* 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. *

* 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'. *

* 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. *

* Collections are output using JSON objects or arrays. * Multisets are output as a map of value to count. *

* 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'. *

* 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, 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. *

* 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}. *

* 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 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, } }