package de.jpaw.bonaparte.core; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; import org.joda.time.Instant; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.joda.time.LocalTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import de.jpaw.bonaparte.enums.BonaNonTokenizableEnum; import de.jpaw.bonaparte.enums.BonaTokenizableEnum; import de.jpaw.bonaparte.pojos.meta.AlphanumericElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.BasicNumericElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.BinaryElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.EnumDataItem; import de.jpaw.bonaparte.pojos.meta.FieldDefinition; import de.jpaw.bonaparte.pojos.meta.IndexType; import de.jpaw.bonaparte.pojos.meta.MiscElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.NumericElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.ObjectReference; import de.jpaw.bonaparte.pojos.meta.TemporalElementaryDataItem; import de.jpaw.bonaparte.pojos.meta.XEnumDataItem; import de.jpaw.bonaparte.util.LongTools; import de.jpaw.enums.XEnum; import de.jpaw.json.JsonEscaper; import de.jpaw.util.Base64; import de.jpaw.util.ByteArray; import de.jpaw.util.ByteBuilder; /** This class natively generates JSON output. It aims for compatibility with the extensions used by the json-io library (@Type class information). * See https://github.com/jdereg/json-io and http://code.google.com/p/json-io/ for json-io source and documentation. * * @author Michael Bischoff (jpaw.de) * * This implementation uses the following logic when writing fields: * - for SCALAR Multiplicity, field name and contents are written * - for LIST, SET, ARRAY, only values are written * - for Map, a special MapMode determines whether the next element output is a key or the data object. * */ public class JsonComposer extends AbstractMessageComposer<IOException> { protected static final DateTimeFormatter LOCAL_DATE_ISO = DateTimeFormat.forPattern("yyyy-MM-dd"); // ISODateTimeFormat.basicDate(); protected static final DateTimeFormatter LOCAL_DATETIME_ISO = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"); // ISODateTimeFormat.basicDateTime(); protected static final DateTimeFormatter LOCAL_TIME_ISO = DateTimeFormat.forPattern("HH:mm:ss'Z'"); // ISODateTimeFormat.basicTime(); protected static final DateTimeFormatter LOCAL_DATETIME_ISO_WITH_MS = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); // ISODateTimeFormat.basicDateTime(); protected static final DateTimeFormatter LOCAL_TIME_ISO_WITH_MS = DateTimeFormat.forPattern("HH:mm:ss.SSS'Z'"); // ISODateTimeFormat.basicTime(); protected String currentClass = "N/A"; protected String remFieldName = null; protected final Appendable out; protected final boolean instantInMillis = false; // instants are integral seconds, as in JWT iat / exp protected final boolean writeNulls; protected final boolean writeTypeInfo; // for every class, also output "@type" and the fully qualified name protected final boolean writePqonInfo; // for every class, also output "@PQON" and the partially qualified name protected final boolean maybeWritePqonInfo; // for every class, also output "@PQON" and the partially qualified name, if the containing record allows subclassing protected final JsonEscaper jsonEscaper; protected boolean currentMapMode = false; protected boolean needFieldSeparator = false; protected boolean needRecordSeparator = false; public static String toJsonStringNoPQON(BonaCustom obj) { if (obj == null) return null; StringBuilder buff = new StringBuilder(4000); JsonComposer bjc = new JsonComposer(buff, false, false, false, false, new BonaparteJsonEscaper(buff)); bjc.setWriteCRs(false); try { bjc.writeRecord(obj); } catch (IOException e) { throw new RuntimeException(e); } return buff.toString(); } public static String toJsonString(BonaCustom obj) { if (obj == null) return null; StringBuilder buff = new StringBuilder(4000); JsonComposer bjc = new JsonComposer(buff); try { bjc.writeRecord(obj); } catch (IOException e) { throw new RuntimeException(e); } return buff.toString(); } public static String toJsonString(Collection<? extends BonaCustom> obj) { if (obj == null) return null; StringBuilder buff = new StringBuilder(4000); JsonComposer bjc = new JsonComposer(buff); try { bjc.writeTransmission(obj); } catch (IOException e) { throw new RuntimeException(e); } return buff.toString(); } public static String toJsonString(Iterable<? extends BonaCustom> obj) { if (obj == null) return null; StringBuilder buff = new StringBuilder(4000); JsonComposer bjc = new JsonComposer(buff); try { bjc.writeTransmission(obj); } catch (IOException e) { // oh yes, sure, StringBuilder throws IOException! throw new RuntimeException(e); } return buff.toString(); } @Override public void writeObject(BonaCustom o) throws IOException { objectOutSub(StaticMeta.OUTER_BONAPORTABLE, o); } public JsonComposer(Appendable out) { this(out, false); } public JsonComposer(Appendable out, boolean writeNulls) { //this(out, writeNulls, false, true, new BonaparteJsonEscaper(out, this)); // this cannot be referenced this.out = out; this.writeNulls = writeNulls; this.writeTypeInfo = false; this.writePqonInfo = false; this.maybeWritePqonInfo = true; this.jsonEscaper = new BonaparteJsonEscaper(out); } public JsonComposer(Appendable out, boolean writeNulls, JsonEscaper jsonEscaper) { this(out, writeNulls, false, false, true, jsonEscaper); } public JsonComposer(Appendable out, boolean writeNulls, boolean writeTypeInfo, boolean writePqonInfo, boolean maybeWritePqonInfo, JsonEscaper jsonEscaper) { this.out = out; this.writeNulls = writeNulls; this.writeTypeInfo = writeTypeInfo; this.writePqonInfo = writePqonInfo; this.maybeWritePqonInfo = maybeWritePqonInfo; this.jsonEscaper = jsonEscaper; } /** Checks if a field separator (',') must be written, and does so if required. Sets the separator to required for the next field. */ protected void writeSeparator() throws IOException { if (needFieldSeparator) out.append(','); else needFieldSeparator = true; } /** Writes a quoted fieldname. We assume that no escaping is required, because all valid identifier characters in Java don't need escaping. */ protected void writeFieldName(FieldDefinition di) throws IOException { if (remFieldName != null) { // from map mode.... Same criteria could be: if MapMode = expect_value writeSeparator(); jsonEscaper.outputUnicodeNoControls(remFieldName); out.append(':'); remFieldName = null; currentMapMode = true; return; } if (di.getName().length() > 0) { writeSeparator(); jsonEscaper.outputUnicodeNoControls(di.getName()); out.append(':'); return; } // else it's the special JSON outer type to be ignored... } protected boolean isListType(FieldDefinition di) { switch (di.getMultiplicity()) { case ARRAY: case LIST: case SET: return true; case MAP: case SCALAR: return false; } return false; } /** Writes a quoted fieldname, if not in an array, or a separator only. */ protected void writeOptionalFieldName(FieldDefinition di) throws IOException { if (isListType(di)) { // inside array: must write without a name writeSeparator(); } else { writeFieldName(di); } } protected void writeOptionalUnquotedString(FieldDefinition di, String s) throws IOException { if (isListType(di)) { // must write a null without a name writeSeparator(); out.append(s == null ? "null" : s); } else if (s != null) { writeFieldName(di); out.append(s); } else if (writeNulls) { writeFieldName(di); out.append("null"); } } protected void writeOptionalQuotedAscii(FieldDefinition di, String s) throws IOException { if (isListType(di)) { // must write a null without a name writeSeparator(); if (s == null) out.append("null"); else jsonEscaper.outputAscii(s); } else if (s != null) { writeFieldName(di); jsonEscaper.outputAscii(s); } else if (writeNulls) { writeFieldName(di); out.append("null"); } } protected void writeOptionalQuotedUnicodeNoControls(FieldDefinition di, String s) throws IOException { if (isListType(di)) { // must write a null without a name writeSeparator(); if (s == null) out.append("null"); else jsonEscaper.outputUnicodeNoControls(s); } else if (s != null) { writeFieldName(di); jsonEscaper.outputUnicodeNoControls(s); } else if (writeNulls) { writeFieldName(di); out.append("null"); } } @Override public void writeNull(FieldDefinition di) throws IOException { if (isListType(di)) { // must write a null without a name writeSeparator(); out.append("null"); } else if (writeNulls) { writeFieldName(di); out.append("null"); } } @Override public void writeNullCollection(FieldDefinition di) throws IOException { if (writeNulls) { writeFieldName(di); out.append("null"); } } @Override public void startTransmission() throws IOException { out.append('['); needRecordSeparator = false; } @Override public void startRecord() throws IOException { } // called for not-null elements only @Override public void startObject(ObjectReference di, BonaCustom obj) throws IOException { out.append('{'); needFieldSeparator = false; if (writeTypeInfo) { // create the class canonical name as a special field, to be compatible to json-io jsonEscaper.outputAscii(MimeTypes.JSON_FIELD_FQON); out.append(':'); jsonEscaper.outputUnicodeNoControls(obj.getClass().getCanonicalName()); needFieldSeparator = true; } if (writePqonInfo || (maybeWritePqonInfo && di.getAllowSubclasses())) { // create the class partially qualified name as a special field, if required writeSeparator(); jsonEscaper.outputAscii(MimeTypes.JSON_FIELD_PQON); out.append(':'); jsonEscaper.outputUnicodeNoControls(obj.ret$PQON()); needFieldSeparator = true; } } @Override public void writeSuperclassSeparator() throws IOException { } @Override public void terminateObject(ObjectReference di, BonaCustom obj) throws IOException { out.append('}'); needFieldSeparator = true; } // called for not-null elements only @Override public void startArray(FieldDefinition di, int currentMembers, int sizeOfElement) throws IOException { writeFieldName(di); out.append('['); needFieldSeparator = false; } @Override public void startMap(FieldDefinition di, int currentMembers) throws IOException { // A map does not exist in JSON. The map is however used to model types with variable field names, such as in Swagger 2.0. // A map of String type keys is used here. // Here, the keys reflect the field names and the value their contents. // In order to allow a mixture of fixed names and variable names, and a mixture of variable names with different types, // the map does not start a new sub object, but rather is serialized into the current object. if (di.getMapIndexType() != IndexType.STRING) throw new IOException(new ObjectValidationException(ObjectValidationException.UNSUPPORTED_MAP_KEY_TYPE, di.getName(), currentClass)); // if (/* !(di instanceof ObjectReference) && */ !(di instanceof AlphanumericElementaryDataItem)) // throw new IOException(new ObjectValidationException(ObjectValidationException.UNSUPPORTED_MAP_VALUE_TYPE, di.getName(), currentClass)); if (currentMapMode) // nested maps are not allowed / possible throw new IOException(new ObjectValidationException(ObjectValidationException.INVALID_SEQUENCE, di.getName(), currentClass)); currentMapMode = true; writeFieldName(di); out.append('{'); needFieldSeparator = false; } // actually this is not called, instead, terminateArray() is called! @Override public void terminateMap() throws IOException { if (!currentMapMode) throw new IOException(new ObjectValidationException(ObjectValidationException.INVALID_SEQUENCE, "?", currentClass)); currentMapMode = false; } @Override public void terminateArray() throws IOException { if (currentMapMode) { // inside map! out.append('}'); currentMapMode = false; } else { // yes, it was an array, list or set! out.append(']'); } needFieldSeparator = true; } @Override public void terminateRecord() throws IOException { if (getWriteCRs()) out.append('\r'); out.append('\n'); } @Override public void terminateTransmission() throws IOException { out.append(']'); } // if required, insert a separator character between records. @Override public void writeRecord(BonaCustom o) throws IOException { if (needRecordSeparator) out.append(','); else needRecordSeparator = true; // next time, I'll need it // super.writeRecord(o); // not working, need a different meta data reference startRecord(); addField(StaticMeta.OUTER_BONAPORTABLE_FOR_JSON, o); terminateRecord(); } @Override public void addField(MiscElementaryDataItem di, boolean b) throws IOException { writeOptionalFieldName(di); out.append(b ? "true" : "false"); } @Override public void addField(MiscElementaryDataItem di, char c) throws IOException { writeOptionalFieldName(di); jsonEscaper.outputUnicodeWithControls(String.valueOf(c)); } @Override public void addField(BasicNumericElementaryDataItem di, double d) throws IOException { writeOptionalFieldName(di); out.append(Double.toString(d)); } @Override public void addField(BasicNumericElementaryDataItem di, float f) throws IOException { writeOptionalFieldName(di); out.append(Float.toString(f)); } @Override public void addField(BasicNumericElementaryDataItem di, byte n) throws IOException { writeOptionalFieldName(di); out.append(Byte.toString(n)); } @Override public void addField(BasicNumericElementaryDataItem di, short n) throws IOException { writeOptionalFieldName(di); out.append(Short.toString(n)); } @Override public void addField(BasicNumericElementaryDataItem di, int n) throws IOException { writeOptionalFieldName(di); out.append(Integer.toString(n)); } @Override public void addField(BasicNumericElementaryDataItem di, long n) throws IOException { LongTools.checkLongOverflow(n); writeOptionalFieldName(di); out.append(Long.toString(n)); } @Override public void addField(AlphanumericElementaryDataItem di, String s) throws IOException { if (di == StaticMeta.MAP_INDEX_META_STRING) { // just remember the field name for later... remFieldName = s; return; } if (isListType(di)) { // in array, list or set // must write a null without a name! writeSeparator(); if (s == null) out.append("null"); else jsonEscaper.outputUnicodeWithControls(s); } else if (s != null) { writeFieldName(di); jsonEscaper.outputUnicodeWithControls(s); } else if (writeNulls) { writeFieldName(di); out.append("null"); } // else don't write at all } protected void objectOutSub(ObjectReference di, BonaCustom obj) throws IOException { // PUSH operation for composer state String previousRemFieldName = remFieldName; remFieldName = null; String previousClass = currentClass; currentClass = di.getName(); boolean previousMapMode = currentMapMode; currentMapMode = false; startObject(di, obj); obj.serializeSub(this); terminateObject(di, obj); // POP operation for composer state currentMapMode = previousMapMode; currentClass = previousClass; remFieldName = previousRemFieldName; } @Override public void addField(ObjectReference di, BonaCustom obj) throws IOException { if (isListType(di)) { // must write a null without a name writeSeparator(); if (obj == null) { out.append("null"); } else { objectOutSub(di, obj); } } else if (obj != null) { writeFieldName(di); objectOutSub(di, obj); } else if (writeNulls) { writeFieldName(di); out.append("null"); } // else don't write at all } @Override public void addField(MiscElementaryDataItem di, UUID n) throws IOException { writeOptionalQuotedAscii(di, n == null ? null : n.toString()); } @Override public void addField(BinaryElementaryDataItem di, ByteArray b) throws IOException { if (b == null) { writeNull(di); } else { ByteBuilder tmp = new ByteBuilder((b.length() * 2) + 4, null); Base64.encodeToByte(tmp, b.getBytes(), 0, b.length()); String s = new String(tmp.getCurrentBuffer(), 0, tmp.length()); writeOptionalQuotedAscii(di, s); } } @Override public void addField(BinaryElementaryDataItem di, byte[] b) throws IOException { if (b == null) { writeNull(di); } else { ByteBuilder tmp = new ByteBuilder((b.length * 2) + 4, null); Base64.encodeToByte(tmp, b, 0, b.length); String s = new String(tmp.getCurrentBuffer(), 0, tmp.length()); writeOptionalQuotedAscii(di, s); } } @Override public void addField(BasicNumericElementaryDataItem di, BigInteger n) throws IOException { writeOptionalUnquotedString(di, n == null ? null : n.toString()); } @Override public void addField(NumericElementaryDataItem di, BigDecimal n) throws IOException { writeOptionalUnquotedString(di, n == null ? null : n.toString()); } @Override public void addField(TemporalElementaryDataItem di, LocalDate t) throws IOException { writeOptionalQuotedAscii(di, t == null ? null : LOCAL_DATE_ISO.print(t)); } @Override public void addField(TemporalElementaryDataItem di, LocalDateTime t) throws IOException { writeOptionalQuotedAscii(di, t == null ? null : di.getFractionalSeconds() > 0 ? LOCAL_DATETIME_ISO_WITH_MS.print(t) : LOCAL_DATETIME_ISO.print(t)); } @Override public void addField(TemporalElementaryDataItem di, LocalTime t) throws IOException { writeOptionalQuotedAscii(di, t == null ? null : di.getFractionalSeconds() > 0 ? LOCAL_TIME_ISO_WITH_MS.print(t) : LOCAL_TIME_ISO.print(t)); } @Override public void addField(TemporalElementaryDataItem di, Instant t) throws IOException { // must be compatible to ExtendedJsonComposer! // writeOptionalQuotedAscii(di, t == null ? null : LOCAL_DATETIME_ISO.print(t)); if (t == null) { writeNull(di); } else { writeFieldName(di); long millis = t.getMillis(); if (instantInMillis) { out.append(Long.toString(millis)); } else { out.append(Long.toString(millis / 1000)); if (di.getFractionalSeconds() > 0) { millis %= 1000; if (millis > 0) out.append(String.format(".%03d", millis)); } } } } @Override public void addEnum(EnumDataItem di, BasicNumericElementaryDataItem ord, BonaNonTokenizableEnum n) throws IOException { writeOptionalUnquotedString(di, n == null ? null : Integer.toString(n.ordinal())); } @Override public void addEnum(EnumDataItem di, AlphanumericElementaryDataItem token, BonaTokenizableEnum n) throws IOException { writeOptionalQuotedUnicodeNoControls(di, n == null ? null : n.getToken()); } @Override public void addEnum(XEnumDataItem di, AlphanumericElementaryDataItem token, XEnum<?> n) throws IOException { writeOptionalQuotedUnicodeNoControls(di, n == null ? null : n.getToken()); } @Override public boolean addExternal(ObjectReference di, Object obj) throws IOException { return false; // perform conversion by default } @Override public void addField(ObjectReference di, Map<String, Object> obj) throws IOException { if (obj == null) { writeNull(di); } else { writeOptionalFieldName(di); jsonEscaper.outputJsonObject(obj); } } @Override public void addField(ObjectReference di, List<Object> obj) throws IOException { if (obj == null) { writeNull(di); } else { writeOptionalFieldName(di); jsonEscaper.outputJsonArray(obj); } } @Override public void addField(ObjectReference di, Object obj) throws IOException { if (obj == null) { writeNull(di); } else { writeOptionalFieldName(di); jsonEscaper.outputJsonElement(obj); } } }