/** * Copyright (C) 2011 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.web.json; import java.io.Closeable; import java.io.Flushable; import java.io.IOException; import java.io.Writer; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.fudgemsg.FudgeContext; import org.fudgemsg.FudgeField; import org.fudgemsg.FudgeFieldType; import org.fudgemsg.FudgeMsg; import org.fudgemsg.FudgeMsgEnvelope; import org.fudgemsg.FudgeRuntimeException; import org.fudgemsg.taxonomy.FudgeTaxonomy; import org.fudgemsg.types.SecondaryFieldTypeBase; import org.fudgemsg.wire.FudgeRuntimeIOException; import org.fudgemsg.wire.FudgeSize; import org.fudgemsg.wire.json.FudgeJSONSettings; import org.fudgemsg.wire.types.FudgeWireType; import org.json.JSONException; import org.json.JSONWriter; import com.google.common.collect.Lists; /** * A Fudge writer that produces JSON. * <p> * This writer writes a Fudge message as JSON. * This can be used for JSON output, or can be used to assist in developing/debugging * a streaming serializer without having to inspect the binary output. * <p> * Please refer to <a href="http://wiki.fudgemsg.org/display/FDG/JSON+Fudge+Messages">JSON Fudge Messages</a> * for details on the representation. */ public class FudgeMsgJSONWriter implements Flushable, Closeable { private static final String BLANK_FIELD_NAME = ""; /** * The taxonomy identifier to use for any messages that are passed without envelopes. */ private static final short DEFAULT_TAXONOMY_ID = 0; /** * The schema version to add to the envelope header for any messages that are passed without envelopes. */ private static final int DEFAULT_MESSAGE_VERSION = 0; /** * The processing directive flags to add to the envelope header for any messages that are passed without envelopes. */ private static final int DEFAULT_MESSAGE_PROCESSING_DIRECTIVES = 0; /** * The fudge context */ private final FudgeContext _fudgeContext; /** * The JSON settings. */ private final FudgeJSONSettings _settings; /** * The underlying writer. */ private final Writer _underlyingWriter; /** * The JSON writer. */ private JSONWriter _writer; /** * Creates a new instance for writing a Fudge stream to a JSON writer. * * @param fudgeContext the Fudge context, not null * @param writer the underlying writer, not null */ public FudgeMsgJSONWriter(final FudgeContext fudgeContext, final Writer writer) { this(fudgeContext, writer, new FudgeJSONSettings()); } /** * Creates a new stream writer for writing Fudge messages in JSON format to a given * {@link Writer}. * * @param fudgeContext the Fudge context, not null * @param writer the underlying writer, not null * @param settings the JSON settings, not null */ public FudgeMsgJSONWriter(final FudgeContext fudgeContext, final Writer writer, final FudgeJSONSettings settings) { if (fudgeContext == null) { throw new NullPointerException("FudgeContext must not be null"); } if (writer == null) { throw new NullPointerException("Writer must not be null"); } if (settings == null) { throw new NullPointerException("FudgeJSONSettings must not be null"); } _settings = settings; _underlyingWriter = writer; _fudgeContext = fudgeContext; } /** * Gets the taxonomy. * @return the taxonomy */ private FudgeTaxonomy getTaxonomy(final int taxonomyId) { if (taxonomyId != DEFAULT_TAXONOMY_ID) { return getFudgeContext().getTaxonomyResolver().resolveTaxonomy((short) taxonomyId); } return null; } public FudgeContext getFudgeContext() { return _fudgeContext; } //------------------------------------------------------------------------- /** * Gets the JSON settings. * * @return the JSON settings, not null */ public FudgeJSONSettings getSettings() { return _settings; } /** * Gets the JSON writer being used, allocating one if necessary. * * @return the writer, not null */ private JSONWriter getWriter() { if (_writer == null) { _writer = new JSONWriter(getUnderlying()); } return _writer; } /** * Discards the JSON writer. * The implementation only allows a single use so we must drop the instance * after each message envelope completes. */ private void clearWriter() { _writer = null; } /** * Gets the underlying {@link Writer} that is wrapped by {@link JSONWriter} instances for messages. * * @return the writer, not null */ public Writer getUnderlying() { return _underlyingWriter; } //------------------------------------------------------------------------- /** * Writes a message with the given taxonomy, schema version and processing directive flags. * * @param message message to write * @param taxonomyId identifier of the taxonomy to use. If the taxonomy is recognized by the {@link FudgeContext} it will be used to reduce field names to ordinals where possible. * @param version schema version * @param processingDirectives processing directive flags */ public void writeMessage(final FudgeMsg message, final int taxonomyId, final int version, final int processingDirectives) { writeMessageEnvelope(new FudgeMsgEnvelope(message, version, processingDirectives), taxonomyId); } /** * Writes a message with the given taxonomy. Default schema version and processing directive flags are used. * * @param message message to write * @param taxonomyId the identifier of the taxonomy to use, if the taxonomy is recognized * by the {@link FudgeContext} it will be used to reduce field names to ordinals where possible. */ public void writeMessage(final FudgeMsg message, final int taxonomyId) { writeMessage(message, taxonomyId, DEFAULT_MESSAGE_VERSION, DEFAULT_MESSAGE_PROCESSING_DIRECTIVES); } /** * Writes a message. Default taxonomy, schema version and processing directive flags are used. * * @param message message to write * @throws NullPointerException if the default taxonomy has not been specified */ public void writeMessage(final FudgeMsg message) { writeMessage(message, DEFAULT_TAXONOMY_ID); } /** * Writes a message envelope with the given taxonomy. * * @param envelope message envelope to write * @param taxonomyId the identifier of the taxonomy to use, if the taxonomy is recognized * by the {@link FudgeContext} it will be used to reduce field names to ordinals where possible. */ public void writeMessageEnvelope(final FudgeMsgEnvelope envelope, final int taxonomyId) { if (envelope == null) { return; } int messageSize = FudgeSize.calculateMessageEnvelopeSize(getTaxonomy(taxonomyId), envelope); writeEnvelopeHeader(envelope.getProcessingDirectives(), envelope.getVersion(), messageSize, taxonomyId); writeData(envelope.getMessage(), taxonomyId); writeMeta(envelope.getMessage(), taxonomyId); envelopeComplete(); } private void writeMeta(FudgeMsg message, int taxonomyId) { try { getWriter().key("meta"); getWriter().object(); } catch (JSONException e) { wrapException("start of data", e); } writeFields(message, taxonomyId, true); try { getWriter().endObject(); } catch (JSONException e) { wrapException("end of data", e); } } private void writeData(FudgeMsg message, int taxonomyId) { writeDataStart(); writeFields(message, taxonomyId, false); writeDataEnd(); } private void writeDataEnd() { try { getWriter().endObject(); } catch (JSONException e) { wrapException("end of data", e); } } private void writeDataStart() { try { getWriter().key("data"); getWriter().object(); } catch (JSONException e) { wrapException("start of data", e); } } private void writeFields(Iterable<FudgeField> fudgeMsg, final int taxonomyId, boolean meta) { Map<String, List<FudgeField>> fieldName2Fields = new LinkedHashMap<String, List<FudgeField>>(); for (FudgeField fudgeField : fudgeMsg) { if (fudgeField.getName() != null) { final List<FudgeField> fields = getFieldList(fieldName2Fields, fudgeField.getName()); fields.add(fudgeField); } else if (fudgeField.getOrdinal() != null) { final List<FudgeField> fields = getFieldList(fieldName2Fields, fudgeField.getOrdinal().toString()); fields.add(fudgeField); } else { final List<FudgeField> fields = getFieldList(fieldName2Fields, BLANK_FIELD_NAME); fields.add(fudgeField); } } for (Entry<String, List<FudgeField>> entry : fieldName2Fields.entrySet()) { List<FudgeField> fields = entry.getValue(); if (!fields.isEmpty()) { if (fields.size() == 1) { FudgeField fudgeField = fields.get(0); writeField(fudgeField.getName(), fudgeField.getOrdinal(), fudgeField.getType(), fudgeField.getValue(), taxonomyId, meta); } else { writeRepeatedFields(fields, taxonomyId, meta); } } } } private void writeRepeatedFields(final List<FudgeField> fields, int taxonomyId, boolean meta) { FudgeField firstField = fields.iterator().next(); try { String key = fudgeFieldStart(firstField.getOrdinal(), firstField.getName(), taxonomyId); if (key != null) { getWriter().array(); for (FudgeField fudgeField : fields) { if (fudgeField.getType().getTypeId() == FudgeWireType.SUB_MESSAGE_TYPE_ID) { fudgeSubMessageStart(); writeFields((FudgeMsg) fudgeField.getValue(), taxonomyId, meta); fudgeSubMessageEnd(); } else { fudgeFieldValue(fudgeField.getType(), fudgeField.getValue(), meta); } } getWriter().endArray(); } } catch (JSONException e) { wrapException("writing repeated fields", e); } } private void writeField(String name, Integer ordinal, FudgeFieldType type, Object fieldValue, final int taxonomyId, boolean meta) { if (fudgeFieldStart(ordinal, name, taxonomyId) != null) { if (type.getTypeId() == FudgeWireType.SUB_MESSAGE_TYPE_ID) { fudgeSubMessageStart(); FudgeMsg subMsg = (FudgeMsg) fieldValue; writeFields(subMsg, taxonomyId, meta); fudgeSubMessageEnd(); } else { fudgeFieldValue(type, fieldValue, meta); } } } @SuppressWarnings("unchecked") private void fudgeFieldValue(FudgeFieldType type, Object fieldValue, boolean meta) { try { if (meta) { String typeIdToString = getSettings().fudgeTypeIdToString(type.getTypeId()); getWriter().value(typeIdToString); } else { if (type instanceof SecondaryFieldTypeBase<?, ?, ?>) { fieldValue = ((SecondaryFieldTypeBase<Object, Object, Object>) type).secondaryToPrimary(fieldValue); } switch (type.getTypeId()) { case FudgeWireType.INDICATOR_TYPE_ID: getWriter().value(null); break; case FudgeWireType.BYTE_ARRAY_TYPE_ID: case FudgeWireType.BYTE_ARRAY_4_TYPE_ID: case FudgeWireType.BYTE_ARRAY_8_TYPE_ID: case FudgeWireType.BYTE_ARRAY_16_TYPE_ID: case FudgeWireType.BYTE_ARRAY_20_TYPE_ID: case FudgeWireType.BYTE_ARRAY_32_TYPE_ID: case FudgeWireType.BYTE_ARRAY_64_TYPE_ID: case FudgeWireType.BYTE_ARRAY_128_TYPE_ID: case FudgeWireType.BYTE_ARRAY_256_TYPE_ID: case FudgeWireType.BYTE_ARRAY_512_TYPE_ID: writeArray((byte[]) fieldValue); break; case FudgeWireType.SHORT_ARRAY_TYPE_ID: writeArray((short[]) fieldValue); break; case FudgeWireType.INT_ARRAY_TYPE_ID: writeArray((int[]) fieldValue); break; case FudgeWireType.LONG_ARRAY_TYPE_ID: writeArray((long[]) fieldValue); break; case FudgeWireType.FLOAT_ARRAY_TYPE_ID: writeArray((float[]) fieldValue); break; case FudgeWireType.DOUBLE_ARRAY_TYPE_ID: writeArray((double[]) fieldValue); break; default: getWriter().value(fieldValue); break; } } } catch (JSONException e) { wrapException("field value", e); } } private void writeArray(final double[] data) throws JSONException { getWriter().array(); for (int i = 0; i < data.length; i++) { getWriter().value(data[i]); } getWriter().endArray(); } private void writeArray(final float[] data) throws JSONException { getWriter().array(); for (int i = 0; i < data.length; i++) { getWriter().value(data[i]); } getWriter().endArray(); } private void writeArray(final long[] data) throws JSONException { getWriter().array(); for (int i = 0; i < data.length; i++) { getWriter().value(data[i]); } getWriter().endArray(); } private void writeArray(final int[] data) throws JSONException { getWriter().array(); for (int i = 0; i < data.length; i++) { getWriter().value(data[i]); } getWriter().endArray(); } private void writeArray(final short[] data) throws JSONException { getWriter().array(); for (int i = 0; i < data.length; i++) { getWriter().value(data[i]); } getWriter().endArray(); } private void writeArray(final byte[] data) throws JSONException { getWriter().array(); for (int i = 0; i < data.length; i++) { getWriter().value(data[i]); } getWriter().endArray(); } /** * Ends the JSON sub-object. */ private void fudgeSubMessageEnd() { try { getWriter().endObject(); } catch (JSONException e) { wrapException("end of submessage", e); } } /** * Starts a sub-object within the JSON object. */ private void fudgeSubMessageStart() { try { getWriter().object(); } catch (JSONException e) { wrapException("start of submessage", e); } } private String fudgeFieldStart(final Integer ordinal, final String name, final int taxonomyId) { String fieldName = null; try { if (getSettings().getPreserveFieldNames()) { if (name != null) { getWriter().key(name); fieldName = name; } else if (ordinal != null) { fieldName = getFieldNameByOrdinal(ordinal, taxonomyId); getWriter().key(fieldName); } else { getWriter().key(BLANK_FIELD_NAME); fieldName = BLANK_FIELD_NAME; } } else { if (ordinal != null) { getWriter().key(ordinal.toString()); fieldName = ordinal.toString(); } else if (name != null) { getWriter().key(name); fieldName = name; } else { getWriter().key(BLANK_FIELD_NAME); fieldName = BLANK_FIELD_NAME; } } } catch (JSONException e) { wrapException("start of field", e); } return fieldName; } private String getFieldNameByOrdinal(final Integer ordinal, final int taxonomyId) { String result = ordinal.toString(); FudgeTaxonomy taxonomy = getTaxonomy(taxonomyId); if (taxonomy != null) { String fieldName = taxonomy.getFieldName(ordinal); if (fieldName != null) { result = fieldName; } } return result; } private List<FudgeField> getFieldList(final Map<String, List<FudgeField>> fieldName2Fields, final String fieldName) { List<FudgeField> fields = fieldName2Fields.get(fieldName); if (fields == null) { fields = Lists.newArrayList(); fieldName2Fields.put(fieldName, fields); } return fields; } private void envelopeComplete() { fudgeEnvelopeEnd(); } /** * Ends the JSON object. */ private void fudgeEnvelopeEnd() { try { getWriter().endObject(); clearWriter(); } catch (JSONException e) { wrapException("end of message", e); } } private void writeEnvelopeHeader(int processingDirectives, int version, int messageSize, int taxonomyId) { fudgeEnvelopeStart(processingDirectives, version, taxonomyId); } /** * Writes a message envelope using the default taxonomy. * * @param envelope message envelope to write * @throws NullPointerException if the default taxonomy has not been specified */ public void writeMessageEnvelope(final FudgeMsgEnvelope envelope) { writeMessageEnvelope(envelope, DEFAULT_TAXONOMY_ID); } //------------------------------------------------------------------------- /** * Wraps a JSON exception (which may in turn wrap {@link IOException} into * either a {@link FudgeRuntimeException} or {@link FudgeRuntimeIOException}. * * @param message message describing the current operation * @param e the originating exception */ private void wrapException(String message, final JSONException e) { message = "Error writing " + message + " to JSON"; if (e.getCause() instanceof IOException) { throw new FudgeRuntimeIOException(message, (IOException) e.getCause()); } else { throw new FudgeRuntimeException(message, e); } } /** * Flushes the underlying {@link Writer}. */ @Override public void flush() { if (getUnderlying() != null) { try { getUnderlying().flush(); } catch (IOException e) { throw new FudgeRuntimeIOException(e); } } } /** * Flushes and closes the underlying {@link Writer}. */ @Override public void close() { flush(); if (getUnderlying() != null) { try { getUnderlying().close(); } catch (IOException ex) { throw new FudgeRuntimeIOException(ex); } } } /** * Begins a JSON object with the processing directives, schema and taxonomy. * @param taxonomyId */ private void fudgeEnvelopeStart(final int processingDirectives, final int schemaVersion, int taxonomyId) { try { getWriter().object(); if ((processingDirectives != 0) && (getSettings().getProcessingDirectivesField() != null)) { getWriter().key(getSettings().getProcessingDirectivesField()).value(processingDirectives); } if ((schemaVersion != 0) && (getSettings().getSchemaVersionField() != null)) { getWriter().key(getSettings().getSchemaVersionField()).value(schemaVersion); } if ((taxonomyId != DEFAULT_TAXONOMY_ID) && (getSettings().getTaxonomyField() != null)) { getWriter().key(getSettings().getTaxonomyField()).value(taxonomyId); } } catch (JSONException e) { wrapException("start of message", e); } } }