JsonWriter.java
package zserio.runtime.json;
import java.io.PrintWriter;
import java.io.Writer;
import java.math.BigInteger;
import zserio.runtime.ZserioBitmask;
import zserio.runtime.ZserioEnum;
import zserio.runtime.ZserioError;
import zserio.runtime.io.BitBuffer;
import zserio.runtime.typeinfo.FieldInfo;
import zserio.runtime.typeinfo.ItemInfo;
import zserio.runtime.typeinfo.TypeInfo;
import zserio.runtime.walker.WalkObserver;
import zserio.runtime.walker.WalkerConst;
/**
* Walker observer which dumps zserio objects to JSON format.
*/
public final class JsonWriter implements WalkObserver, AutoCloseable
{
/**
* Constructor.
*
* @param out Writer to use for writing.
*/
public JsonWriter(Writer out)
{
this(out, null);
}
/**
* Constructor.
*
* @param out Writer to use for writing.
* @param indent Indent as a number of ' ' to be used for indentation.
*/
public JsonWriter(Writer out, int indent)
{
this(out, new String(new char[indent]).replace('\0', ' '));
}
/**
* Constructor.
*
* @param out Writer to use for writing.
* @param indent Indent as a string to be used for indentation.
*/
public JsonWriter(Writer out, String indent)
{
this.out = new PrintWriter(out);
this.indent = indent;
this.itemSeparator = indent == null ? DEFAULT_ITEM_SEPARATOR : DEFAULT_ITEM_SEPARATOR_WITH_INDENT;
}
/**
* Sets custom item separator.
*
* Use with caution since setting of a wrong separator can lead to invalid JSON output.
*
* @param itemSeparator Item separator to set.
*/
public void setItemSeparator(String itemSeparator)
{
this.itemSeparator = itemSeparator;
}
/**
* Sets custom key separator.
*
* Use with caution since setting of a wrong separator can lead to invalid JSON output.
*
* @param keySeparator Key separator to set.
*/
public void setKeySeparator(String keySeparator)
{
this.keySeparator = keySeparator;
}
/**
* Configuration for writing of enumerable types.
*/
public enum EnumerableFormat
{
/** Print as JSON integral value. */
NUMBER,
/**
* Print as JSON string according to the following rules:
*
* <ol>
* <li> Enums
* <ul>
* <li> when an exact match with an enumerable item is found, the item name is used - e.g.
* <code>"FIRST"</code>,
* <li> when no exact match is found, it's an invalid value, the integral value is converted to string
* and an appropriate comment is included - e.g.
* <code>"10 /<span>*</span> no match <span>*</span>/"</code>.
* </ul>
* <li> Bitmasks
* <ul>
* <li> when an exact mach with or-ed bitmask values is found, it's used - e.g.
* <code>"READ | WRITE"</code>,
* <li> when no exact match is found, but some or-ed values match, the integral value is converted
* to string and the or-ed values are included in a comment - e.g.
* <code>"127 /<span>*</span> READ | CREATE <span>*</span>/"</code>,
* <li> when no match is found at all, the integral value is converted to string and an appropriate
* comment is included - e.g. <code>"13 /<span>*</span> no match <span>*</span>/"</code>.
* </ul>
* </ol>
*/
STRING
}
;
/**
* Sets preferred formatting for enumerable types.
*
* @param enumerableFormat Enumerable format to use.
*/
public void setEnumerableFormat(EnumerableFormat enumerableFormat)
{
this.enumerableFormat = enumerableFormat;
}
@Override
public void close()
{
out.close();
}
@Override
public void beginRoot(Object compound)
{
beginObject();
}
@Override
public void endRoot(Object compound)
{
endObject();
flush();
}
@Override
public void beginArray(Object array, FieldInfo fieldInfo)
{
beginItem();
writeKey(fieldInfo.getSchemaName());
beginArray();
}
@Override
public void endArray(Object array, FieldInfo fieldInfo)
{
endArray();
endItem();
}
@Override
public void beginCompound(Object compound, FieldInfo fieldInfo, int elementIndex)
{
beginItem();
if (elementIndex == WalkerConst.NOT_ELEMENT)
writeKey(fieldInfo.getSchemaName());
beginObject();
}
@Override
public void endCompound(Object compound, FieldInfo fieldInfo, int elementIndex)
{
endObject();
endItem();
}
@Override
public void visitValue(Object value, FieldInfo fieldInfo, int elementIndex)
{
beginItem();
if (elementIndex == WalkerConst.NOT_ELEMENT)
writeKey(fieldInfo.getSchemaName());
writeValue(value, fieldInfo);
endItem();
}
private void beginItem()
{
if (!isFirst)
out.write(itemSeparator);
if (indent != null)
out.write(System.lineSeparator());
writeIndent();
}
private void endItem()
{
isFirst = false;
}
private void beginObject()
{
out.write('{');
isFirst = true;
level += 1;
}
private void endObject()
{
if (indent != null)
out.write(System.lineSeparator());
level -= 1;
writeIndent();
out.write('}');
}
private void beginArray()
{
out.write('[');
isFirst = true;
level += 1;
}
private void endArray()
{
if (indent != null)
out.write(System.lineSeparator());
level -= 1;
writeIndent();
out.write(']');
}
private void writeIndent()
{
if (indent != null && !indent.isEmpty())
{
for (int i = 0; i < level; ++i)
out.write(indent);
}
}
private void writeKey(String key)
{
JsonEncoder.encodeString(out, key);
out.write(keySeparator);
flush();
}
private void writeValue(Object value, FieldInfo fieldInfo)
{
if (value == null)
{
JsonEncoder.encodeNull(out);
return;
}
switch (fieldInfo.getTypeInfo().getJavaType())
{
case BOOLEAN:
JsonEncoder.encodeBool(out, (boolean)value);
break;
case BYTE:
case SHORT:
case INT:
case LONG:
JsonEncoder.encodeIntegral(out, ((Number)value).longValue());
break;
case BIG_INTEGER:
JsonEncoder.encodeIntegral(out, (BigInteger)value);
break;
case FLOAT:
case DOUBLE:
JsonEncoder.encodeFloatingPoint(out, ((Number)value).doubleValue());
break;
case BYTES:
writeBytes((byte[])value);
break;
case STRING:
JsonEncoder.encodeString(out, (String)value);
break;
case BIT_BUFFER:
writeBitBuffer((BitBuffer)value);
break;
case ENUM:
if (enumerableFormat == EnumerableFormat.STRING)
writeStringifiedEnum((ZserioEnum)value, fieldInfo.getTypeInfo());
else
JsonEncoder.encodeIntegral(out, ((ZserioEnum)value).getGenericValue());
break;
case BITMASK:
if (enumerableFormat == EnumerableFormat.STRING)
writeStringifiedBitmask((ZserioBitmask)value, fieldInfo.getTypeInfo());
else
JsonEncoder.encodeIntegral(out, ((ZserioBitmask)value).getGenericValue());
break;
default:
throw new ZserioError("JsonWriter: Unexpected not-null value of type '" +
fieldInfo.getTypeInfo().getSchemaName() + "'!");
}
flush();
}
private void writeBytes(byte[] bytes)
{
beginObject();
beginItem();
writeKey("buffer");
beginArray();
for (byte byteValue : bytes)
{
beginItem();
// note that we don't want to have negative numbers in bytes
JsonEncoder.encodeIntegral(out, Byte.toUnsignedInt(byteValue));
endItem();
}
endArray();
endItem();
endObject();
}
private void writeBitBuffer(BitBuffer bitBuffer)
{
beginObject();
beginItem();
writeKey("buffer");
beginArray();
for (byte byteValue : bitBuffer.getBuffer())
{
beginItem();
// note that we don't want to have negative numbers in bit buffer
JsonEncoder.encodeIntegral(out, Byte.toUnsignedInt(byteValue));
endItem();
}
endArray();
endItem();
beginItem();
writeKey("bitSize");
JsonEncoder.encodeIntegral(out, bitBuffer.getBitSize());
endItem();
endObject();
}
private void writeStringifiedEnum(ZserioEnum zserioEnum, TypeInfo typeInfo)
{
final BigInteger enumValue = numberToBigInteger(zserioEnum.getGenericValue());
for (ItemInfo itemInfo : typeInfo.getEnumItems())
{
// exact match
if (enumValue.equals(itemInfo.getValue()))
{
JsonEncoder.encodeString(out, itemInfo.getSchemaName());
return;
}
}
// no match
JsonEncoder.encodeString(out, enumValue.toString() + " /* no match */");
}
private void writeStringifiedBitmask(ZserioBitmask zserioBitmask, TypeInfo typeInfo)
{
StringBuilder stringValue = new StringBuilder();
final BigInteger bitmaskValue = numberToBigInteger(zserioBitmask.getGenericValue());
BigInteger valueCheck = BigInteger.ZERO;
for (ItemInfo itemInfo : typeInfo.getBitmaskValues())
{
final boolean isZero = itemInfo.getValue().equals(BigInteger.ZERO);
if ((!isZero && bitmaskValue.and(itemInfo.getValue()).equals(itemInfo.getValue())) ||
(isZero && bitmaskValue.equals(BigInteger.ZERO)))
{
valueCheck = valueCheck.or(itemInfo.getValue());
if (stringValue.length() > 0)
stringValue.append(" | ");
stringValue.append(itemInfo.getSchemaName());
}
}
if (stringValue.length() == 0)
{
// no match
stringValue.append(bitmaskValue.toString());
stringValue.append(" /* no match */");
}
else if (!bitmaskValue.equals(valueCheck))
{
// partial match
stringValue =
new StringBuilder(bitmaskValue.toString())
.append(" /* partial match: ")
.append(stringValue.toString())
.append(" */");
}
// else exact match
JsonEncoder.encodeString(out, stringValue.toString());
}
private static BigInteger numberToBigInteger(Number number)
{
if (number instanceof BigInteger)
return (BigInteger)number;
else
return BigInteger.valueOf(number.longValue());
}
private void flush()
{
out.flush();
if (out.checkError())
throw new ZserioError("JsonWriter: Output stream error occurred!");
}
/**
* Default item separator used when indent is not set (i.e. is null).
*/
public static final String DEFAULT_ITEM_SEPARATOR = ", ";
/**
* Default item separator used when indent is not null.
*/
public static final String DEFAULT_ITEM_SEPARATOR_WITH_INDENT = ",";
/**
* Default key separator.
*/
public static final String DEFAULT_KEY_SEPARATOR = ": ";
/**
* Default configuration for enumerable types.
*/
public static final EnumerableFormat DEFAULT_ENUMERABLE_FORMAT = EnumerableFormat.STRING;
private final PrintWriter out;
private final String indent;
private String itemSeparator;
private String keySeparator = DEFAULT_KEY_SEPARATOR;
private EnumerableFormat enumerableFormat = DEFAULT_ENUMERABLE_FORMAT;
private boolean isFirst = true;
private int level = 0;
}