Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2015 The MsgCodec Authors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.cinnober.msgcodec.json; import java.io.IOException; import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.LinkedList; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; import com.cinnober.msgcodec.Accessor; import com.cinnober.msgcodec.CreateAccessor; import com.cinnober.msgcodec.DecodeException; import com.cinnober.msgcodec.Epoch; import com.cinnober.msgcodec.Factory; import com.cinnober.msgcodec.GroupDef; import com.cinnober.msgcodec.ObjectInstantiationException; import com.cinnober.msgcodec.SymbolMapping; import com.cinnober.msgcodec.TypeDef; import com.cinnober.msgcodec.util.TimeFormat; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import java.text.ParseException; import java.util.BitSet; import java.util.List; /** * Writes and reads values to and from JsonGenerator and JsonParser respectively. * * @author Mikael Brannstrom * @param <T> the value type */ public abstract class JsonValueHandler<T> { static final String TYPE_FIELD = "$type"; public static final JsonValueHandler<Byte> INT8 = new Int8Handler(); public static final JsonValueHandler<Short> INT16 = new Int16Handler(); public static final JsonValueHandler<Integer> INT32 = new Int32Handler(); public static final JsonValueHandler<Long> INT64 = new Int64Handler(false); public static final JsonValueHandler<Long> INT64_SAFE = new Int64Handler(true); public static final JsonValueHandler<Byte> UINT8 = new UInt8Handler(); public static final JsonValueHandler<Short> UINT16 = new UInt16Handler(); public static final JsonValueHandler<Character> CHAR = new CharacterHandler(); public static final JsonValueHandler<Integer> UINT32 = new UInt32Handler(); public static final JsonValueHandler<Long> UINT64 = new UInt64Handler(false); public static final JsonValueHandler<Long> UINT64_SAFE = new UInt64Handler(true); public static final JsonValueHandler<String> STRING = new StringHandler(); public static final JsonValueHandler<byte[]> BINARY = new BinaryHandler(); public static final JsonValueHandler<Boolean> BOOLEAN = new BooleanHandler(); public static final JsonValueHandler<BigDecimal> DECIMAL = new DecimalHandler(false); public static final JsonValueHandler<BigDecimal> DECIMAL_SAFE = new DecimalHandler(true); public static final JsonValueHandler<BigDecimal> BIGDECIMAL = new BigDecimalHandler(false); public static final JsonValueHandler<BigDecimal> BIGDECIMAL_SAFE = new BigDecimalHandler(true); public static final JsonValueHandler<BigInteger> BIGINT = new BigIntHandler(false); public static final JsonValueHandler<BigInteger> BIGINT_SAFE = new BigIntHandler(true); public static final JsonValueHandler<Float> FLOAT32 = new Float32Handler(); public static final JsonValueHandler<Double> FLOAT64 = new Float64Handler(); static final long MAX_SAFE_INTEGER = 9007199254740991L; static final long MIN_SAFE_INTEGER = -9007199254740991L; /** * Returns a the basic value handler for the specified type definition and Java class. * * <p><b>Note:</b> the types SEQUENCE, REFERENCE and DYNAMIC_REFERENCE are not supported. * * @param <T> the java type * @param type the type definition, not null. * @param javaClass the java class, not null. * @param symbolMapping the symbol mapping if this is an enum or a sequence of an enum * @param jsSafe true if unsafe JavaScript numeric values should be encoded as strings, otherwise false. * @param accessor the field accessor, used to determine if a dummy handler is needed * @return the json value handler, not null. */ @SuppressWarnings({ "unchecked", "rawtypes" }) public static <T> JsonValueHandler<T> getValueHandler(TypeDef type, Class<T> javaClass, SymbolMapping<T> symbolMapping, boolean jsSafe, Accessor<?, ?> accessor) { if (accessor.getClass() == CreateAccessor.class) { switch (type.getType()) { case ENUM: return new JsonValueHandler.DummyEnumHandler((TypeDef.Enum) type, javaClass); default: // Do nothing for other types } } switch (type.getType()) { case INT8: checkType(javaClass, byte.class, Byte.class); return (JsonValueHandler<T>) JsonValueHandler.INT8; case INT16: checkType(javaClass, short.class, Short.class); return (JsonValueHandler<T>) JsonValueHandler.INT16; case INT32: checkType(javaClass, int.class, Integer.class); return (JsonValueHandler<T>) JsonValueHandler.INT32; case INT64: checkType(javaClass, long.class, Long.class); return (JsonValueHandler<T>) (jsSafe ? JsonValueHandler.INT64_SAFE : JsonValueHandler.INT64); case UINT8: checkType(javaClass, byte.class, Byte.class); return (JsonValueHandler<T>) JsonValueHandler.UINT8; case CHAR: checkType(javaClass, char.class, Character.class); return (JsonValueHandler<T>) JsonValueHandler.CHAR; case UINT16: checkType(javaClass, short.class, Short.class); return (JsonValueHandler<T>) JsonValueHandler.UINT16; case UINT32: checkType(javaClass, int.class, Integer.class); return (JsonValueHandler<T>) JsonValueHandler.UINT32; case UINT64: checkType(javaClass, long.class, Long.class); return (JsonValueHandler<T>) (jsSafe ? JsonValueHandler.UINT64_SAFE : JsonValueHandler.UINT64); case STRING: checkType(javaClass, String.class); return ((TypeDef.StringUnicode) type).hasMaxSize() ? (JsonValueHandler<T>) new StringHandler(((TypeDef.StringUnicode) type).getMaxSize()) : (JsonValueHandler<T>) JsonValueHandler.STRING; case BOOLEAN: checkType(javaClass, boolean.class, Boolean.class); return (JsonValueHandler<T>) JsonValueHandler.BOOLEAN; case BINARY: checkType(javaClass, byte[].class); return ((TypeDef.Binary) type).hasMaxSize() ? (JsonValueHandler<T>) new BinaryHandler(((TypeDef.Binary) type).getMaxSize()) : (JsonValueHandler<T>) JsonValueHandler.BINARY; case DECIMAL: checkType(javaClass, BigDecimal.class); return (JsonValueHandler<T>) (jsSafe ? JsonValueHandler.DECIMAL_SAFE : JsonValueHandler.DECIMAL); case BIGDECIMAL: checkType(javaClass, BigDecimal.class); return (JsonValueHandler<T>) (jsSafe ? JsonValueHandler.BIGDECIMAL_SAFE : JsonValueHandler.BIGDECIMAL); case BIGINT: checkType(javaClass, BigInteger.class); return (JsonValueHandler<T>) (jsSafe ? JsonValueHandler.BIGINT_SAFE : JsonValueHandler.BIGINT); case FLOAT32: checkType(javaClass, float.class, Float.class); return (JsonValueHandler<T>) JsonValueHandler.FLOAT32; case FLOAT64: checkType(javaClass, double.class, Double.class); return (JsonValueHandler<T>) JsonValueHandler.FLOAT64; case ENUM: return new JsonValueHandler.EnumHandler<T>(symbolMapping); case TIME: if (javaClass.equals(Date.class)) { return (JsonValueHandler<T>) new JsonValueHandler.DateTimeHandler((TypeDef.Time) type); } else if (javaClass.equals(Integer.class) || javaClass.equals(int.class)) { return (JsonValueHandler<T>) new JsonValueHandler.IntTimeHandler((TypeDef.Time) type); } else if (javaClass.equals(Long.class) || javaClass.equals(long.class)) { return (JsonValueHandler<T>) new JsonValueHandler.LongTimeHandler((TypeDef.Time) type); } else { throw new IllegalArgumentException("Illegal time java class: " + javaClass); } case SEQUENCE: case REFERENCE: case DYNAMIC_REFERENCE: throw new IllegalArgumentException("Illegal type: " + type); default: throw new RuntimeException("Unhandled type: " + type.getType()); } } private static <T> void checkType(Class<T> type, Class<?>... expTypes) { for (Class<?> expType : expTypes) { if (type == expType) { return; } } throw new IllegalArgumentException("Illegal type java class: " + type); } static boolean isJavaScriptSafeSigned(long value) { return MIN_SAFE_INTEGER <= value && value <= MAX_SAFE_INTEGER; } static boolean isJavaScriptSafeUnsigned(long value) { return 0L <= value && value <= MAX_SAFE_INTEGER; } static boolean isJavaScriptSafeSigned(BigInteger value) { int bitlen = value.bitLength(); return bitlen <= 52 || (bitlen == 53 && isJavaScriptSafeSigned(value.longValue())); } static boolean isJavaScriptSafeSigned(BigDecimal value) { // the number of decimal digits a double can uniquely identify is 15 return value.precision() <= 15; } /** * Write the value to the specified json generator. * @param value the value, not null. * @param g the json generator to write to, not null. * @throws IOException if the json generator throws an exception. */ public abstract void writeValue(T value, JsonGenerator g) throws IOException; /** * Read a value from the specified json parser. * @param p the json parser to read from, not null. * @return the value, not null. * @throws IOException if the json parser throws an exception. */ public abstract T readValue(JsonParser p) throws IOException; static class Int8Handler extends JsonValueHandler<Byte> { private Int8Handler() { } @Override public void writeValue(Byte value, JsonGenerator g) throws IOException { g.writeNumber(value); } @Override public Byte readValue(JsonParser p) throws IOException { return (byte) p.getIntValue(); } } static class Int16Handler extends JsonValueHandler<Short> { private Int16Handler() { } @Override public void writeValue(Short value, JsonGenerator g) throws IOException { g.writeNumber(value); } @Override public Short readValue(JsonParser p) throws IOException { return (short) p.getIntValue(); } } static class Int32Handler extends JsonValueHandler<Integer> { private Int32Handler() { } @Override public void writeValue(Integer value, JsonGenerator g) throws IOException { g.writeNumber(value); } @Override public Integer readValue(JsonParser p) throws IOException { return p.getIntValue(); } } static class Int64Handler extends JsonValueHandler<Long> { private final boolean jsSafe; private Int64Handler(boolean jsSafe) { this.jsSafe = jsSafe; } @Override public void writeValue(Long value, JsonGenerator g) throws IOException { long v = value.longValue(); if (jsSafe && !isJavaScriptSafeUnsigned(v)) { g.writeString(Long.toString(v)); } else { g.writeNumber(v); } } @Override public Long readValue(JsonParser p) throws IOException { switch (p.getCurrentToken()) { case VALUE_NUMBER_INT: return p.getLongValue(); case VALUE_STRING: return Long.parseLong(p.getText()); default: throw new DecodeException("Found " + p.getCurrentToken() + " while parsing an int64"); } } } static class UInt8Handler extends JsonValueHandler<Byte> { private UInt8Handler() { } @Override public void writeValue(Byte value, JsonGenerator g) throws IOException { g.writeNumber(value.byteValue() & 0xff); } @Override public Byte readValue(JsonParser p) throws IOException { return (byte) p.getIntValue(); } } static class UInt16Handler extends JsonValueHandler<Short> { private UInt16Handler() { } @Override public void writeValue(Short value, JsonGenerator g) throws IOException { g.writeNumber(value.shortValue() & 0xffff); } @Override public Short readValue(JsonParser p) throws IOException { return (short) p.getIntValue(); } } static class CharacterHandler extends JsonValueHandler<Character> { private CharacterHandler() { } @Override public void writeValue(Character value, JsonGenerator g) throws IOException { g.writeNumber(value.charValue() & 0xffff); } @Override public Character readValue(JsonParser p) throws IOException { return (char) p.getIntValue(); } } static class UInt32Handler extends JsonValueHandler<Integer> { private UInt32Handler() { } @Override public void writeValue(Integer value, JsonGenerator g) throws IOException { g.writeNumber(value.intValue() & 0xffffffffL); } @Override public Integer readValue(JsonParser p) throws IOException { return (int) p.getLongValue(); } } static class UInt64Handler extends JsonValueHandler<Long> { private static final BigInteger TWO_POW_64 = BigInteger.ONE.shiftLeft(64); private final boolean jsSafe; private UInt64Handler(boolean jsSafe) { this.jsSafe = jsSafe; } @Override public void writeValue(Long value, JsonGenerator g) throws IOException { long v = value.longValue(); if (jsSafe && !isJavaScriptSafeUnsigned(value)) { if (value < 0) { g.writeString(TWO_POW_64.add(BigInteger.valueOf(v)).toString()); } else { g.writeString(Long.toString(v)); } } else { if (v < 0) { g.writeNumber(TWO_POW_64.add(BigInteger.valueOf(v)).toString()); } else { g.writeNumber(v); } } } @Override public Long readValue(JsonParser p) throws IOException { // TODO: we're not validating that the parsed value is positive and less than 2^64 switch (p.getCurrentToken()) { case VALUE_NUMBER_INT: return p.getBigIntegerValue().longValue(); case VALUE_STRING: return new BigInteger(p.getText()).longValue(); default: throw new DecodeException("Found " + p.getCurrentToken() + " while parsing an uint64"); } } } static class StringHandler extends JsonValueHandler<String> { final int maxSize; private StringHandler() { this(-1); } private StringHandler(int maxSize) { this.maxSize = maxSize; } @Override public void writeValue(String value, JsonGenerator g) throws IOException { if (maxSize != -1 && value.length() > maxSize) { // PENDING: should actually check number of bytes (not chars), but that is expensive throw new IllegalArgumentException( "String length (" + value.length() + ") exceeds max size " + maxSize); } g.writeString(value); } @Override public String readValue(JsonParser p) throws IOException { String value = p.getText(); if (maxSize != -1 && value.length() > maxSize) { // PENDING: should actually check number of bytes (not chars), but that is expensive throw new DecodeException("String length (" + value.length() + ") exceeds max size " + maxSize); } return value; } } static class BinaryHandler extends JsonValueHandler<byte[]> { final int maxSize; private BinaryHandler() { this(-1); } private BinaryHandler(int maxSize) { this.maxSize = maxSize; } @Override public void writeValue(byte[] value, JsonGenerator g) throws IOException { if (maxSize != -1 && value.length > maxSize) { throw new IllegalArgumentException( "Binary length (" + value.length + ") exceeds max size " + maxSize); } g.writeBinary(value); } @Override public byte[] readValue(JsonParser p) throws IOException { byte[] value = p.getBinaryValue(); if (maxSize != -1 && value.length > maxSize) { throw new DecodeException("Binary length (" + value.length + ") exceeds max size " + maxSize); } return value; } } static class BooleanHandler extends JsonValueHandler<Boolean> { private BooleanHandler() { } @Override public void writeValue(Boolean value, JsonGenerator g) throws IOException { g.writeBoolean(value); } @Override public Boolean readValue(JsonParser p) throws IOException { return p.getBooleanValue(); } } static class DecimalHandler extends JsonValueHandler<BigDecimal> { private final boolean jsSafe; private DecimalHandler(boolean jsSafe) { this.jsSafe = jsSafe; } @Override public void writeValue(BigDecimal value, JsonGenerator g) throws IOException { TypeDef.checkDecimal(value); if (jsSafe && !isJavaScriptSafeSigned(value)) { g.writeString(value.toString()); } else { g.writeNumber(value); } } @Override public BigDecimal readValue(JsonParser p) throws IOException { switch (p.getCurrentToken()) { case VALUE_NUMBER_INT: case VALUE_NUMBER_FLOAT: return checkRange(p.getDecimalValue()); case VALUE_STRING: return checkRange(new BigDecimal(p.getText())); default: throw new DecodeException("Found " + p.getCurrentToken() + " while parsing a decimal"); } } private static BigDecimal checkRange(BigDecimal value) throws DecodeException { try { return TypeDef.checkDecimal(value); } catch (IllegalArgumentException e) { throw new DecodeException(e.getMessage()); } } } static class BigDecimalHandler extends JsonValueHandler<BigDecimal> { private final boolean jsSafe; private BigDecimalHandler(boolean jsSafe) { this.jsSafe = jsSafe; } @Override public void writeValue(BigDecimal value, JsonGenerator g) throws IOException { if (jsSafe && !isJavaScriptSafeSigned(value)) { g.writeString(value.toString()); } else { g.writeNumber(value); } } @Override public BigDecimal readValue(JsonParser p) throws IOException { switch (p.getCurrentToken()) { case VALUE_NUMBER_INT: case VALUE_NUMBER_FLOAT: return p.getDecimalValue(); case VALUE_STRING: return new BigDecimal(p.getText()); default: throw new DecodeException("Found " + p.getCurrentToken() + " while parsing a big decimal"); } } } static class BigIntHandler extends JsonValueHandler<BigInteger> { private final boolean jsSafe; private BigIntHandler(boolean jsSafe) { this.jsSafe = jsSafe; } @Override public void writeValue(BigInteger value, JsonGenerator g) throws IOException { if (jsSafe && !isJavaScriptSafeSigned(value)) { g.writeString(value.toString()); } else { g.writeNumber(value); } } @Override public BigInteger readValue(JsonParser p) throws IOException { switch (p.getCurrentToken()) { case VALUE_NUMBER_INT: return p.getBigIntegerValue(); case VALUE_STRING: return new BigInteger(p.getText()); default: throw new DecodeException("Found " + p.getCurrentToken() + " while parsing a big integer"); } } } static class Float32Handler extends JsonValueHandler<Float> { private Float32Handler() { } @Override public void writeValue(Float value, JsonGenerator g) throws IOException { g.writeNumber(value); } @Override public Float readValue(JsonParser p) throws IOException { switch (p.getCurrentToken()) { case VALUE_NUMBER_FLOAT: case VALUE_NUMBER_INT: return p.getFloatValue(); case VALUE_STRING: switch (p.getText()) { case "NaN": return Float.NaN; case "Infinity": return Float.POSITIVE_INFINITY; case "-Infinity": return Float.NEGATIVE_INFINITY; default: throw new DecodeException("Illegal float32 string value: " + p.getText()); } default: throw new DecodeException("Found " + p.getCurrentToken() + " while parsing a big integer"); } } } static class Float64Handler extends JsonValueHandler<Double> { private Float64Handler() { } @Override public void writeValue(Double value, JsonGenerator g) throws IOException { g.writeNumber(value); } @Override public Double readValue(JsonParser p) throws IOException { switch (p.getCurrentToken()) { case VALUE_NUMBER_FLOAT: case VALUE_NUMBER_INT: return p.getDoubleValue(); case VALUE_STRING: switch (p.getText()) { case "NaN": return Double.NaN; case "Infinity": return Double.POSITIVE_INFINITY; case "-Infinity": return Double.NEGATIVE_INFINITY; default: throw new DecodeException("Illegal float64 string value: " + p.getText()); } default: throw new DecodeException("Found " + p.getCurrentToken() + " while parsing a big integer"); } } } public static abstract class TimeHandler<T> extends JsonValueHandler<T> { private final Epoch epoch; private final TimeUnit unit; private final TimeFormat timeFormat; public TimeHandler(TypeDef.Time type) { this.epoch = type.getEpoch(); this.unit = type.getUnit(); this.timeFormat = TimeFormat.getTimeFormat(unit, epoch); } /** * Convert the value to a long value for the specified epoch and time unit. * * @param value the time value, not null. * @return the long value for the specified epoch and time unit. */ protected abstract long convertToLong(T value); /** * Convert the value from a long value for the specified epoch and time unit. * * @param value the long value for the specified epoch and time unit. * @return the time value, not null. */ protected abstract T convertFromLong(long value); @Override public void writeValue(T value, JsonGenerator g) throws IOException { long timeValue = convertToLong(value); String timeStr = timeFormat.format(timeValue); g.writeString(timeStr); } @Override public T readValue(JsonParser p) throws IOException { try { String s = p.getText(); long timeValue = timeFormat.parse(s); return convertFromLong(timeValue); } catch (ParseException e) { throw new DecodeException("Could not parse time", e); } } } public static class IntTimeHandler extends TimeHandler<Integer> { public IntTimeHandler(TypeDef.Time type) { super(type); } @Override protected long convertToLong(Integer value) { return value; } @Override protected Integer convertFromLong(long value) { return (int) value; } } public static class LongTimeHandler extends TimeHandler<Long> { public LongTimeHandler(TypeDef.Time type) { super(type); } @Override protected long convertToLong(Long value) { return value; } @Override protected Long convertFromLong(long value) { return value; } } private static long getTimeInMillis(TimeUnit unit) { switch (unit) { case MILLISECONDS: return 1; case SECONDS: return 1000; case MINUTES: return 60 * 1000; case HOURS: return 60 * 60 * 1000; case DAYS: return 24 * 60 * 60 * 1000; default: throw new IllegalArgumentException("Date does not support " + unit); } } private static long getEpochOffset(Epoch epoch) { switch (epoch) { case UNIX: return 0; case Y2K: return 946706400000L; case MIDNIGHT: return 0; default: throw new IllegalArgumentException("Date does not support " + epoch); } } public static class DateTimeHandler extends TimeHandler<Date> { private final long timeUnitInMillis; private final long epochOffset; /** * Create a new date type handler. * @param type the time type, not null. */ public DateTimeHandler(TypeDef.Time type) { super(type); timeUnitInMillis = getTimeInMillis(type.getUnit()); epochOffset = getEpochOffset(type.getEpoch()); } @Override protected long convertToLong(Date value) { return (value.getTime() - epochOffset) / timeUnitInMillis; } @Override protected Date convertFromLong(long value) { return new Date(value * timeUnitInMillis + epochOffset); } } public static class EnumHandler<E> extends JsonValueHandler<E> { private final SymbolMapping<E> symbolMapping; /** * Create a new Java enum handler. * @param symbolMapping the symbol mapping of the enum */ public EnumHandler(SymbolMapping<E> symbolMapping) { Objects.requireNonNull(symbolMapping); this.symbolMapping = symbolMapping; } @Override public void writeValue(E value, JsonGenerator g) throws IOException { g.writeString(symbolMapping.getName(value)); } @Override public E readValue(JsonParser p) throws IOException { String str = p.getText(); E value = symbolMapping.lookup(str); if (value == null) { throw new DecodeException("Not a valid symbol: " + str); } return value; } } public static class DummyEnumHandler<E extends Enum<E>> extends JsonValueHandler<E> { public DummyEnumHandler(TypeDef.Enum type, Class<E> enumClass) { } @Override public void writeValue(E value, JsonGenerator g) throws IOException { g.writeString((String) null); } @Override public E readValue(JsonParser p) throws IOException { return null; } } @SuppressWarnings({ "rawtypes", "unchecked" }) public static class ListSequenceHandler extends JsonValueHandler<List> { private final JsonValueHandler componentHandler; /** * Create a list sequence handler. * @param componentHandler the handler for the list component values, not null. */ public ListSequenceHandler(JsonValueHandler componentHandler) { this.componentHandler = componentHandler; } @Override public void writeValue(List list, JsonGenerator g) throws IOException { g.writeStartArray(); for (Object value : list) { componentHandler.writeValue(value, g); } g.writeEndArray(); } @Override public List readValue(JsonParser p) throws IOException { List list = new ArrayList(); // start array already consumed while (p.nextToken() != JsonToken.END_ARRAY) { list.add(componentHandler.readValue(p)); } return list; } public JsonValueHandler getComponentHandler() { return componentHandler; } } @SuppressWarnings({ "rawtypes", "unchecked" }) public static class ArraySequenceHandler extends JsonValueHandler<Object> { private final JsonValueHandler componentHandler; private final Class<?> componentType; /** * Create an array sequence handler. * @param componentHandler the handler for the list component values, not null. * @param componentType the component java type, not null. */ public ArraySequenceHandler(JsonValueHandler componentHandler, Class<?> componentType) { this.componentHandler = componentHandler; this.componentType = componentType; } @Override public void writeValue(Object array, JsonGenerator g) throws IOException { g.writeStartArray(); int length = Array.getLength(array); for (int i = 0; i < length; i++) { Object value = Array.get(array, i); componentHandler.writeValue(value, g); } g.writeEndArray(); } @Override public Object readValue(JsonParser p) throws IOException { Collection list = new LinkedList(); // start array already consumed while (p.nextToken() != JsonToken.END_ARRAY) { list.add(componentHandler.readValue(p)); } Object array = Array.newInstance(componentType, list.size()); int i = 0; for (Object value : list) { Array.set(array, i++, value); } return array; } public JsonValueHandler getComponentHandler() { return componentHandler; } } @SuppressWarnings({ "rawtypes", "unchecked" }) public static class FieldHandler { private final String name; private final Accessor accessor; private final boolean required; private final int requiredSlot; private final JsonValueHandler valueHandler; public FieldHandler(String name, Accessor accessor, boolean required, int requiredSlot, JsonValueHandler valueHandler) { this.name = name; this.accessor = accessor; this.required = required; this.requiredSlot = requiredSlot; this.valueHandler = valueHandler; } void writeValue(Object group, JsonGenerator g) throws IOException { Object value = accessor.getValue(group); if (accessor instanceof CreateAccessor) { if (value != null) { g.writeFieldName(name); valueHandler.writeValue(value, g); } return; } if (value != null) { g.writeFieldName(name); valueHandler.writeValue(value, g); } else if (required) { throw new IllegalArgumentException("Missing required field value: " + name); } } void readValue(Object group, JsonParser p) throws IOException { Object value = valueHandler.readValue(p); accessor.setValue(group, value); } void readNull() throws IOException { if (required) { throw new DecodeException("Found null for non-optional field: " + name); } } boolean isRequired() { return required; } int getRequiredSlot() { return requiredSlot; } String getName() { return name; } public JsonValueHandler getValueHandler() { return valueHandler; } } @SuppressWarnings({ "rawtypes" }) public static class StaticGroupHandler extends JsonValueHandler<Object> { private final String name; private final Factory factory; private Map<String, FieldHandler> fields; private int numRequiredFields; StaticGroupHandler(GroupDef group) { this.name = group.getName(); this.factory = group.getBinding().getFactory(); } void init(Map<String, FieldHandler> fields) { this.fields = fields; this.numRequiredFields = (int) fields.values().stream().mapToInt(FieldHandler::getRequiredSlot) .filter(i -> i >= 0).count(); } public int getNumRequiredFields() { return numRequiredFields; } void writeValue(Object value, JsonGenerator g, boolean dynamic) throws IOException { g.writeStartObject(); if (dynamic) { g.writeFieldName(TYPE_FIELD); g.writeString(name); } for (FieldHandler field : fields.values()) { field.writeValue(value, g); } g.writeEndObject(); } @Override public void writeValue(Object value, JsonGenerator g) throws IOException { writeValue(value, g, false); } @Override public Object readValue(JsonParser p) throws IOException { Object group; try { group = factory.newInstance(); } catch (ObjectInstantiationException e) { throw new DecodeException(e); } readValue(group, p); return group; } void readValue(Object group, JsonParser p) throws IOException { // startObject has already been read BitSet requiredFields = new BitSet(numRequiredFields); requiredFields.set(0, numRequiredFields); while (p.nextToken() == JsonToken.FIELD_NAME) { String fieldName = p.getText(); FieldHandler fieldHandler = fields.get(fieldName); if (fieldHandler == null) { throw new DecodeException("Unknown field: " + fieldName); } if (p.nextToken() == JsonToken.VALUE_NULL) { fieldHandler.readNull(); } else { fieldHandler.readValue(group, p); if (fieldHandler.isRequired()) { requiredFields.clear(fieldHandler.getRequiredSlot()); } } } if (!requiredFields.isEmpty()) { StringBuilder str = new StringBuilder("Missing required ") .append(requiredFields.cardinality() == 1 ? "field" : "fields").append(": "); boolean comma = false; for (FieldHandler fieldHandler : fields.values()) { if (fieldHandler.isRequired() && requiredFields.get(fieldHandler.getRequiredSlot())) { if (comma) { str.append(", "); } str.append(fieldHandler.getName()); comma = true; } } throw new DecodeException(str.toString()); } } Map<String, FieldHandler> getFields() { return fields; } } public static class DynamicGroupHandler extends JsonValueHandler<Object> { private final JsonCodec jsonCodec; DynamicGroupHandler(JsonCodec jsonCodec) { this.jsonCodec = jsonCodec; } @Override public void writeValue(Object value, JsonGenerator g) throws IOException { StaticGroupHandler groupHandler = jsonCodec.lookupGroupByValue(value); if (groupHandler == null) { throw new IllegalArgumentException("Cannot encode group (unknown type)"); } groupHandler.writeValue(value, g, true); } @Override public Object readValue(JsonParser p) throws IOException { if (p.nextToken() != JsonToken.FIELD_NAME) { throw new DecodeException("Expected field"); } String groupName; if (p.getText().equals(TYPE_FIELD)) { p.nextToken(); // field value groupName = p.getText(); } else { TypeScannerJsonParser p2; if (p instanceof TypeScannerJsonParser) { p2 = (TypeScannerJsonParser) p; } else { p2 = new TypeScannerJsonParser(p); p = p2; } groupName = p2.findType(); } StaticGroupHandler groupHandler = jsonCodec.lookupGroupByName(groupName); if (groupHandler == null) { throw new DecodeException("Unknown type: " + groupName); } return groupHandler.readValue(p); } } }