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

com.cedarsoftware.io.JsonWriter Maven / Gradle / Ivy

There is a newer version: 4.30.0
Show newest version
package com.cedarsoftware.io;

import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.EnumSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;

import com.cedarsoftware.io.reflect.Accessor;
import com.cedarsoftware.util.FastWriter;

import static com.cedarsoftware.io.JsonValue.ID;
import static com.cedarsoftware.io.JsonValue.SHORT_ID;
import static com.cedarsoftware.io.JsonValue.SHORT_TYPE;
import static com.cedarsoftware.io.JsonValue.TYPE;

/**
 * Output a Java object graph in JSON format.  This code handles cyclic
 * references and can serialize any Object graph without requiring a class
 * to be 'Serializable' or have any specific methods on it.
 * 
  • * Call the static method: {@code JsonWriter.objectToJson(employee)}. This will * convert the passed in 'employee' instance into a JSON String.
  • *
  • Using streams: *
         JsonWriter writer = new JsonWriter(stream);
     *     writer.write(employee);
     *     writer.close();
    * This will write the 'employee' object to the passed in OutputStream. *
*

That's it. This can be used as a debugging tool. Output an object * graph using the above code. Use the JsonWriter PRETTY_PRINT option to * format the JSON to be human-readable. *
*

This will output any object graph deeply (or null). Object references are * properly handled. For example, if you had A->B, B->C, and C->A, then * A will be serialized with a B object in it, B will be serialized with a C * object in it, and then C will be serialized with a reference to A (ref), not a * redefinition of A.

*
* * @author John DeRegnaucourt ([email protected]) *
* Copyright (c) Cedar Software LLC *

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

* License *

* 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. */ public class JsonWriter implements WriterContext, Closeable, Flushable { private static final Object[] byteStrings = new Object[256]; private static final String NEW_LINE = System.lineSeparator(); private static final Long ZERO = 0L; private final WriteOptions writeOptions; private final Map objVisited = new IdentityHashMap<>(); private final Map objsReferenced = new IdentityHashMap<>(); private final Writer out; private long identity = 1; private int depth = 0; static { for (short i = -128; i <= 127; i++) { char[] chars = Integer.toString(i).toCharArray(); byteStrings[i + 128] = chars; } } /** * Implement this interface to customize the JSON output for a given class. */ public interface JsonClassWriter { /** * When write() is called, it is expected that subclasses will write the appropriate JSON * to the passed in Writer. * * @param o Object to be written in JSON format. * @param showType boolean indicating whether to show @type. * @param output Writer destination to where the actual JSON is written. * @param context WriterContext to get writeOptions and other write access * @throws IOException if thrown by the writer. Will be caught at a higher level and wrapped in JsonIoException. */ default void write(Object o, boolean showType, Writer output, WriterContext context) throws IOException { } /** * @param context WriterContext to get writeOptions and other write access * @return boolean true if the class being written has a primitive (non-object) form. Default is false since * most custom writers will not have a primitive form. */ default boolean hasPrimitiveForm(WriterContext context) { return false; } /** * This default implementation will call the more basic writePrimitiveForm that does not take arguments. No need * to override this method unless you need access to the args. * @param o Object to be written * @param output Writer destination to where the actual JSON is written. * @param context WriterContext to get access to writeOptions and writing tools * @throws IOException if thrown by the writer. Will be caught at a higher level and wrapped in JsonIoException. */ default void writePrimitiveForm(Object o, Writer output, WriterContext context) throws IOException { } } /** * @param out OutputStream to which the JSON will be written. Uses the default WriteOptions. * @see WriteOptions */ public JsonWriter(OutputStream out) { this(out, null); } /** * @param out OutputStream to which the JSON output will be written. * @param writeOptions WriteOptions containing many feature options to control the JSON output. Can be null, * in which case the default WriteOptions will be used. * @see WriteOptions Javadoc. */ public JsonWriter(OutputStream out, WriteOptions writeOptions) { this.out = new FastWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); this.writeOptions = writeOptions == null ? WriteOptionsBuilder.getDefaultWriteOptions() : writeOptions; } public WriteOptions getWriteOptions() { return writeOptions; } /** * Map containing all objects that were visited within input object graph */ protected Map getObjVisited() { return objVisited; } /** * Map containing all objects that were referenced within input object graph. */ public Map getObjsReferenced() { return objsReferenced; } /** * Tab the output left (less indented) * @throws IOException */ public void tabIn() throws IOException { tab(out, 1); } /** * Add newline (\n) to output * @throws IOException */ public void newLine() throws IOException { tab(out, 0); } /** * Tab the output right (more indented) * @throws IOException */ public void tabOut() throws IOException { tab(out, -1); } /** * tab the JSON output by the given number of characters specified by delta. * @param output Writer being used for JSON output. * @param delta int number of characters to tab. * @throws IOException */ private void tab(Writer output, int delta) throws IOException { if (!writeOptions.isPrettyPrint()) { return; } output.write(NEW_LINE); depth += delta; for (int i=0; i < depth; i++) { output.write(" "); } } /** * Write the passed in object (o) to the JSON output stream, if and only if, there is a custom * writer associated to the Class of object (o). * @param o Object to be (potentially written) * @param showType boolean indicating whether to show @type. * @param output Writer where the actual JSON is being written to. * @return boolean true if written, false is there is no custom writer for the passed in object. */ public boolean writeUsingCustomWriter(Object o, boolean showType, Writer output) { Class c = o.getClass(); if (writeOptions.isNotCustomWrittenClass(c)) { return false; } try { return writeCustom(c, o, !writeOptions.isNeverShowingType() && showType, output); } catch (Exception e) { throw new JsonIoException("Unable to write custom formatted object:", e); } } /** * Write the passed in array element to the JSON output, if any only if, there is a custom writer * for the class of the instance 'o'. * @param arrayComponentClass Class type of the array * @param o Object instance to write * @param showType boolean indicating whether @type should be output. * @param output Writer to write the JSON to (if there is a custom writer for o's Class). * @return true if the array element was written, false otherwise. */ public boolean writeArrayElementIfMatching(Class arrayComponentClass, Object o, boolean showType, Writer output) { if (!arrayComponentClass.isAssignableFrom(o.getClass()) || writeOptions.isNotCustomWrittenClass(o.getClass())) { return false; } try { return writeCustom(arrayComponentClass, o, showType, output); } catch (IOException e) { throw new JsonIoException("Unable to write custom formatted object as array element:", e); } } /** * Perform the actual custom writing for an array element that has a custom writer. * @param arrayComponentClass Class type of the array * @param o Object instance to write * @param showType boolean indicating whether @type should be output. * @param output Writer to write the JSON to (if there is a custom writer for o's Class). * @return true if the array element was written, false otherwise. */ protected boolean writeCustom(Class arrayComponentClass, Object o, boolean showType, Writer output) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } JsonClassWriter closestWriter = writeOptions.getCustomWriter(o.getClass()); if (closestWriter == null) { return false; } if (writeOptionalReference(o)) { return true; } boolean referenced = objsReferenced.containsKey(o); if (closestWriter.hasPrimitiveForm(this)) { if ((!referenced && !showType) || closestWriter instanceof Writers.JsonStringWriter) { closestWriter.writePrimitiveForm(o, output, this); return true; } } output.write('{'); tabIn(); if (referenced) { writeId(getId(o)); if (showType) { output.write(','); newLine(); } } if (showType) { writeType(o.getClass().getName(), output); } if (referenced || showType) { output.write(','); newLine(); } closestWriter.write(o, showType || referenced, output, this); tabOut(); output.write('}'); return true; } /** * Write the passed in Java object in JSON format. * @param obj Object any Java Object or JsonObject. */ public void write(Object obj) { traceReferences(obj); objVisited.clear(); try { writeImpl(obj, true); } catch (JsonIoException e) { throw e; } catch (Exception e) { throw new JsonIoException("Error writing object to JSON:", e); } flush(); objVisited.clear(); objsReferenced.clear(); } /** * Walk object graph and visit each instance, following each field, each Collection, Map and so on. * Tracks visited to handle cycles and to determine if an item is referenced elsewhere. If an * object is never referenced more than once, no @id field needs to be emitted for it. * @param root Object to be deeply traced. The objVisited and objsReferenced Maps will be written to * during the trace. */ protected void traceReferences(Object root) { if (root == null) { return; } final Deque stack = new ArrayDeque<>(); stack.addFirst(root); final Map visited = objVisited; final Map referenced = objsReferenced; while (!stack.isEmpty()) { final Object obj = stack.removeFirst(); if (!writeOptions.isNonReferenceableClass(obj.getClass())) { Long id = visited.get(obj); if (id != null) { // Only write an object once. if (id.equals(ZERO)) { // 2nd time this object has been seen, so give it a unique ID and mark it referenced id = identity++; visited.put(obj, id); referenced.put(obj, id); } continue; } else { // Initially, mark an object with 0 as the ID, in case it is never referenced, // we don't waste the memory to store a Long instance that is never used. visited.put(obj, ZERO); } } final Class clazz = obj.getClass(); if (clazz.isArray()) { if (!writeOptions.isNonReferenceableClass(clazz.getComponentType())) { // Speed up: do not traceReferences of primitives, they cannot reference anything final int len = Array.getLength(obj); for (int i = 0; i < len; i++) { final Object o = Array.get(obj, i); if (o != null) { // Slight perf gain (null is legal) stack.addFirst(o); } } } } else if (Map.class.isAssignableFrom(clazz)) { // Speed up - logically walk maps, as opposed to following their internal structure. try { Map map = (Map) obj; for (final Object item : map.entrySet()) { final Entry entry = (Entry) item; Object key = entry.getKey(); Object value = entry.getValue(); if (value != null && !writeOptions.isNonReferenceableClass(value.getClass())) { stack.addFirst(value); } if (key != null && !writeOptions.isNonReferenceableClass(key.getClass())) { stack.addFirst(key); } } } catch (UnsupportedOperationException e) { // Some kind of Map that does not support .entrySet() - some Maps throw UnsupportedOperation for // this API. Do not attempt any further tracing of references. Likely a ClassLoader field or // something unusual like that. } } else if (Collection.class.isAssignableFrom(clazz)) { for (final Object item : (Collection)obj) { if (item != null && !writeOptions.isNonReferenceableClass(item.getClass())) { stack.addFirst(item); } } } else { // Speed up: do not traceReferences of non-referenceable classes if (!writeOptions.isNonReferenceableClass(obj.getClass())) { traceFields(stack, obj); } } } } /** * Reach-ability trace to visit all objects within the graph to be written. * This API will handle any object, using either reflection APIs or by * consulting a specified includedFields map if provided. * @param stack Deque used to manage descent into graph (rather than using Java stack.) This allows for * much larger graph processing. * @param obj Object root of graph * the JDK reflection operations. This allows a subset of the actual fields on an object to be serialized. */ protected void traceFields(final Deque stack, final Object obj) { // If caller has special Field specifier for a given class // then use it, otherwise use reflection. Collection fields = writeOptions.getAccessorsForClass(obj.getClass()); for (final Accessor accessor : fields) { MetaUtils.safelyIgnoreException(() -> { final Object o = accessor.retrieve(obj); if (o != null && !writeOptions.isNonReferenceableClass(o.getClass())) { // Trace through objects that can reference other objects stack.addFirst(o); } }); } } private boolean writeOptionalReference(Object obj) throws IOException { if (obj == null || writeOptions.isNonReferenceableClass(obj.getClass())) { return false; } final Writer output = this.out; if (objVisited.containsKey(obj)) { // Only write (define) an object once in the JSON stream, otherwise emit a @ref String id = getId(obj); if (id == null) { // Test for null because of Weak/Soft references being gc'd during serialization. return false; } output.write(writeOptions.isShortMetaKeys() ? "{\"@r\":" : "{\"@ref\":"); output.write(id); output.write('}'); return true; } // Mark the object as visited by putting it in the Map (this map is re-used / clear()'d after walk()). objVisited.put(obj, null); return false; } /** * Main entry point (mostly used internally, but may be called from a Custom JSON writer). * This method will write out whatever object type it is given, including JsonObject's. * It will handle null, detecting if a custom writer should be called, array, array of * JsonObject, Map, Map of JsonObjects, Collection, Collection of JsonObject, any regular * object, or a JsonObject representing a regular object. * @param obj Object to be written * @param showType if set to true, the @type tag will be output. * @throws IOException if one occurs on the underlying output stream. */ public void writeImpl(Object obj, boolean showType) throws IOException { // For security - write instances of these classes out as null if (obj == null || obj instanceof ProcessBuilder || obj instanceof Process || obj instanceof ClassLoader || obj instanceof Constructor || obj instanceof Method || obj instanceof Field) { out.write("null"); return; } if (writeOptions.isNeverShowingType()) { showType = false; } if (writeUsingCustomWriter(obj, showType, out) || writeOptionalReference(obj)) { return; } if (obj.getClass().isArray()) { writeArray(obj, showType); } else if (obj instanceof EnumSet) { writeEnumSet((EnumSet) obj); } else if (obj instanceof Collection) { writeCollection((Collection) obj, showType); } else if (obj instanceof JsonObject) { // symmetric support for writing Map of Maps representation back as equivalent JSON format. JsonObject jObj = (JsonObject) obj; if (jObj.isArray()) { writeJsonObjectArray(jObj, showType); } else if (jObj.isCollection()) { writeJsonObjectCollection(jObj, showType); } else if (jObj.isMap()) { if (!writeJsonObjectMapWithStringKeys(jObj, showType)) { writeJsonObjectMap(jObj, showType); } } else { writeJsonObjectObject(jObj, showType); } } else if (obj instanceof Map) { // Map instanceof check must be after JsonObject check above (JsonObject is a subclass of Map) if (!writeMapWithStringKeys((Map) obj, showType)) { writeMap((Map) obj, showType); } } else { writeObject(obj, showType, false); } } private void writeId(final String id) throws IOException { out.write(writeOptions.isShortMetaKeys() ? "\"" + SHORT_ID + "\":" : "\"" + ID + "\":"); out.write(id == null ? "0" : id); } private void writeType(String name, Writer output) throws IOException { if (writeOptions.isNeverShowingType()) { return; } output.write(writeOptions.isShortMetaKeys() ? "\"" + SHORT_TYPE + "\":\"" : "\"" + TYPE + "\":\""); String alias = writeOptions.getTypeNameAlias(name); output.write(alias); output.write('"'); } private void writePrimitive(final Object obj, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } if (obj instanceof Long && getWriteOptions().isWriteLongsAsStrings()) { if (showType) { out.write('{'); writeType("long", out); out.write(','); } JsonClassWriter writer = getWriteOptions().getCustomWriter(Long.class); writer.write(obj, showType, out, this); if (showType) { out.write('}'); } } else if (!writeOptions.isAllowNanAndInfinity() && obj instanceof Double && (Double.isNaN((Double) obj) || Double.isInfinite((Double) obj))) { out.write("null"); } else if (!writeOptions.isAllowNanAndInfinity() && obj instanceof Float && (Float.isNaN((Float) obj) || Float.isInfinite((Float) obj))) { out.write("null"); } else { out.write(obj.toString()); } } private void writeArray(final Object array, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } Class arrayType = array.getClass(); int len = Array.getLength(array); boolean referenced = objsReferenced.containsKey(array); boolean typeWritten = showType && !(arrayType.equals(Object[].class)); final Writer output = this.out; // performance opt: place in final local for quicker access if (typeWritten || referenced) { output.write('{'); tabIn(); } if (referenced) { writeId(getId(array)); output.write(','); newLine(); } if (typeWritten) { writeType(arrayType.getName(), output); output.write(','); newLine(); } if (len == 0) { if (typeWritten || referenced) { output.write(writeOptions.isShortMetaKeys() ? "\"@e\":[]" : "\"@items\":[]"); tabOut(); output.write('}'); } else { output.write("[]"); } return; } if (typeWritten || referenced) { output.write(writeOptions.isShortMetaKeys() ? "\"@e\":[" : "\"@items\":["); } else { output.write('['); } tabIn(); final int lenMinus1 = len - 1; // Intentionally processing each primitive array type in separate // custom loop for speed. All of them could be handled using // reflective Array.get() but it is slower. I chose speed over code length. if (byte[].class == arrayType) { writeByteArray((byte[]) array, lenMinus1); } else if (char[].class == arrayType) { writeJsonUtf8String(output, new String((char[]) array)); } else if (short[].class == arrayType) { writeShortArray((short[]) array, lenMinus1); } else if (int[].class == arrayType) { writeIntArray((int[]) array, lenMinus1); } else if (long[].class == arrayType) { writeLongArray((long[]) array, lenMinus1); } else if (float[].class == arrayType) { writeFloatArray((float[]) array, lenMinus1); } else if (double[].class == arrayType) { writeDoubleArray((double[]) array, lenMinus1); } else if (boolean[].class == arrayType) { writeBooleanArray((boolean[]) array, lenMinus1); } else { final Class componentClass = array.getClass().getComponentType(); for (int i = 0; i < len; i++) { final Object value = Array.get(array, i); if (value == null) { output.write("null"); } else { final boolean forceType = isForceType(value.getClass(), componentClass); if (!writeArrayElementIfMatching(componentClass, value, forceType, output)) { writeImpl(value, forceType); } } if (i != lenMinus1) { // Make sure no bogus comma at the end of the array output.write(','); newLine(); } } } tabOut(); output.write(']'); if (typeWritten || referenced) { tabOut(); output.write('}'); } } private void writeBooleanArray(boolean[] booleans, int lenMinus1) throws IOException { final Writer output = this.out; for (int i = 0; i < lenMinus1; i++) { output.write(booleans[i] ? "true," : "false,"); } output.write(Boolean.toString(booleans[lenMinus1])); } private void writeDoubleArray(double[] doubles, int lenMinus1) throws IOException { final Writer output = this.out; final JsonClassWriter writer = getWriteOptions().getCustomWriter(Double.class); for (int i = 0; i < lenMinus1; i++) { writer.write(doubles[i], false, output, this); output.write(','); } writer.write(doubles[lenMinus1], false, output, this); } private void writeFloatArray(float[] floats, int lenMinus1) throws IOException { final Writer output = this.out; final JsonClassWriter writer = getWriteOptions().getCustomWriter(Float.class); for (int i = 0; i < lenMinus1; i++) { writer.write(floats[i], false, output, this); output.write(','); } writer.write(floats[lenMinus1], false, output, this); } private void writeLongArray(long[] longs, int lenMinus1) throws IOException { final Writer output = this.out; JsonClassWriter writer = getWriteOptions().getCustomWriter(long.class); for (int i = 0; i < lenMinus1; i++) { writer.write(longs[i], false, output, this); output.write(','); } writer.write(longs[lenMinus1], false, output, this); } private void writeIntArray(int[] ints, int lenMinus1) throws IOException { final Writer output = this.out; for (int i = 0; i < lenMinus1; i++) { output.write(Integer.toString(ints[i])); output.write(','); } output.write(Integer.toString(ints[lenMinus1])); } private void writeShortArray(short[] shorts, int lenMinus1) throws IOException { final Writer output = this.out; for (int i = 0; i < lenMinus1; i++) { output.write(Integer.toString(shorts[i])); output.write(','); } output.write(Integer.toString(shorts[lenMinus1])); } private void writeByteArray(byte[] bytes, int lenMinus1) throws IOException { final Writer output = this.out; final Object[] byteStrs = byteStrings; for (int i = 0; i < lenMinus1; i++) { output.write((char[]) byteStrs[bytes[i] + 128]); output.write(','); } output.write((char[]) byteStrs[bytes[lenMinus1] + 128]); } private void writeCollection(Collection col, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } final Writer output = this.out; boolean referenced = this.objsReferenced.containsKey(col); boolean isEmpty = col.isEmpty(); if (referenced || showType) { output.write('{'); tabIn(); } else if (isEmpty) { output.write('['); } writeIdAndTypeIfNeeded(col, showType, referenced); if (isEmpty) { if (referenced || showType) { tabOut(); output.write('}'); } else { output.write(']'); } return; } beginCollection(showType, referenced); Iterator i = col.iterator(); writeElements(output, i); tabOut(); output.write(']'); if (showType || referenced) { // Finished object, as it was output as an object if @id or @type was output tabOut(); output.write("}"); } } private void writeElements(Writer output, Iterator i) throws IOException { while (i.hasNext()) { writeCollectionElement(i.next()); if (i.hasNext()) { output.write(','); newLine(); } } } private void writeIdAndTypeIfNeeded(Object col, boolean showType, boolean referenced) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } if (referenced) { writeId(getId(col)); } if (showType) { if (referenced) { out.write(','); newLine(); } writeType(col.getClass().getName(), out); } } private void beginCollection(boolean showType, boolean referenced) throws IOException { if (showType || referenced) { out.write(','); newLine(); out.write(writeOptions.isShortMetaKeys() ? "\"@e\":[" : "\"@items\":["); } else { out.write('['); } tabIn(); } private void writeJsonObjectArray(JsonObject jObj, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } int len = jObj.getLength(); Class arrayClass; Class jsonObjectType = jObj.getJavaType(); if (jsonObjectType == null || Object[].class.equals(jsonObjectType)) { arrayClass = Object[].class; } else { arrayClass = jsonObjectType; } final Writer output = this.out; final boolean isObjectArray = Object[].class == arrayClass; final Class componentClass = arrayClass.getComponentType(); boolean referenced = adjustIfReferenced(jObj); boolean typeWritten = showType && !isObjectArray; if (typeWritten || referenced) { output.write('{'); tabIn(); } if (referenced) { writeId(Long.toString(jObj.id)); output.write(','); newLine(); } if (typeWritten) { writeType(arrayClass.getName(), output); output.write(','); newLine(); } if (len == 0) { if (typeWritten || referenced) { output.write(writeOptions.isShortMetaKeys() ? "\"@e\":[]" : "\"@items\":[]"); tabOut(); output.write("}"); } else { output.write("[]"); } return; } if (typeWritten || referenced) { output.write(writeOptions.isShortMetaKeys() ? "\"@e\":[" : "\"@items\":["); } else { output.write('['); } tabIn(); Object[] items = jObj.getJsonArray(); final int lenMinus1 = len - 1; for (int i = 0; i < len; i++) { final Object value = items[i]; if (value == null) { output.write("null"); } else { final boolean forceType = isForceType(value.getClass(), componentClass); if (writeArrayElementIfMatching(componentClass, value, forceType, output)) { } else if (Character.class == componentClass || char.class == componentClass) { writeJsonUtf8String(output, (String) value); } else if (value instanceof String) { writeJsonUtf8String(output, (String) value); } else if (value instanceof Boolean || value instanceof Long || value instanceof Double) { writePrimitive(value, forceType); } else { // Specific Class-type arrays - only force type when // the instance is derived from array base class. writeImpl(value, forceType); } } if (i != lenMinus1) { output.write(','); newLine(); } } tabOut(); output.write(']'); if (typeWritten || referenced) { tabOut(); output.write('}'); } } private void writeJsonObjectCollection(JsonObject jObj, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } Class colClass = jObj.getJavaType(); boolean referenced = adjustIfReferenced(jObj); final Writer output = this.out; int len = jObj.getLength(); if (referenced || showType || len == 0) { output.write('{'); tabIn(); } if (referenced) { writeId(String.valueOf(jObj.id)); } if (showType) { if (referenced) { output.write(','); newLine(); } writeType(colClass.getName(), output); } if (len == 0) { tabOut(); output.write('}'); return; } beginCollection(showType, referenced); Object[] items = jObj.getJsonArray(); final int itemsLen = items.length; final int itemsLenMinus1 = itemsLen - 1; for (int i=0; i < itemsLen; i++) { writeCollectionElement(items[i]); if (i != itemsLenMinus1) { output.write(','); newLine(); } } tabOut(); output.write("]"); if (showType || referenced) { tabOut(); output.write('}'); } } private void writeJsonObjectMap(JsonObject jObj, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } final Writer output = this.out; showType = emitIdAndTypeIfNeeded(jObj, showType, output); if (jObj.isEmpty()) { // Empty tabOut(); output.write('}'); return; } if (showType) { output.write(','); newLine(); } writeMapToEnd(jObj, output); } private boolean writeJsonObjectMapWithStringKeys(JsonObject jObj, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } if (writeOptions.isForceMapOutputAsTwoArrays() || !ensureJsonPrimitiveKeys(jObj)) { return false; } Writer output = this.out; emitIdAndTypeIfNeeded(jObj, showType, output); if (jObj.isEmpty()) { // Empty tabOut(); output.write('}'); return true; } if (showType) { output.write(','); newLine(); } return writeMapBody(jObj.entrySet().iterator()); } private boolean emitIdAndTypeIfNeeded(JsonObject jObj, boolean showType, Writer output) throws IOException { boolean referenced = adjustIfReferenced(jObj); output.write('{'); tabIn(); if (referenced) { writeId(String.valueOf(jObj.getId())); } if (showType) { if (referenced) { output.write(','); newLine(); } String type = jObj.getJavaTypeName(); if (type != null) { writeType(type, output); } else { // type not displayed showType = false; } } return showType; } /** * Write fields of an Object (JsonObject) */ private void writeJsonObjectObject(JsonObject jObj, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } final Writer output = this.out; boolean referenced = adjustIfReferenced(jObj); showType = showType && jObj.getJavaType() != null; output.write('{'); tabIn(); if (referenced) { writeId(String.valueOf(jObj.id)); } Class type = null; if (showType) { if (referenced) { output.write(','); newLine(); } writeType(jObj.getJavaTypeName(), output); type = jObj.getJavaType(); } if (jObj.isEmpty()) { tabOut(); output.write('}'); return; } if (showType || referenced) { output.write(','); newLine(); } Iterator> i = jObj.entrySet().iterator(); boolean first = true; while (i.hasNext()) { Map.Entry entry = i.next(); if (writeOptions.isSkipNullFields() && entry.getValue() == null) { continue; } if (!first) { output.write(','); newLine(); } first = false; final String fieldName = (String) entry.getKey(); output.write('"'); output.write(fieldName); output.write("\":"); Object value = entry.getValue(); if (value == null) { output.write("null"); } else if (value instanceof BigDecimal || value instanceof BigInteger) { writeImpl(value, !doesValueTypeMatchFieldType(type, fieldName, value)); } else if (value instanceof Number || value instanceof Boolean) { output.write(value.toString()); } else if (value instanceof String) { writeJsonUtf8String(output, (String) value); } else if (value instanceof Character) { writeJsonUtf8String(output, String.valueOf(value)); } else { writeImpl(value, !doesValueTypeMatchFieldType(type, fieldName, value)); } } tabOut(); output.write('}'); } private boolean adjustIfReferenced(JsonObject jObj) { Long idx = objsReferenced.get(jObj); if (!jObj.hasId() && idx != null && idx > 0) { // Referenced object that needs an ID copied to it. jObj.id = idx; } return objsReferenced.containsKey(jObj) && jObj.hasId(); } // Hopefully this method goes away when the converters are done. // We're asking for a DeepDeclaredFields to check One Field Type // Its cached, but we shouldn't have to do that here. I would think if its a known converter type // that we avoid this section all together. private boolean doesValueTypeMatchFieldType(Class type, String fieldName, Object value) { if (type != null) { Map fieldMap = writeOptions.getDeepDeclaredFields(type); Field field = fieldMap.get(fieldName); return field != null && field.getType().equals(value.getClass()); } return false; } private void writeMap(Map map, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } final Writer output = this.out; boolean referenced = this.objsReferenced.containsKey(map); output.write('{'); tabIn(); if (referenced) { writeId(getId(map)); } if (showType) { if (referenced) { output.write(','); newLine(); } writeType(map.getClass().getName(), output); } if (map.isEmpty()) { tabOut(); output.write('}'); return; } if (showType || referenced) { output.write(','); newLine(); } writeMapToEnd(map, output); } private void writeMapToEnd(Map map, Writer output) throws IOException { output.write(writeOptions.isShortMetaKeys() ? "\"@k\":[" : "\"@keys\":["); tabIn(); Iterator i = map.keySet().iterator(); writeElements(output, i); tabOut(); output.write("],"); newLine(); output.write(writeOptions.isShortMetaKeys() ? "\"@e\":[" : "\"@items\":["); tabIn(); i = map.values().iterator(); writeElements(output, i); tabOut(); output.write(']'); tabOut(); output.write('}'); } private boolean writeMapWithStringKeys(Map map, boolean showType) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } if (writeOptions.isForceMapOutputAsTwoArrays() || !ensureJsonPrimitiveKeys(map)) { return false; } boolean referenced = this.objsReferenced.containsKey(map); out.write('{'); tabIn(); writeIdAndTypeIfNeeded(map, showType, referenced); if (map.isEmpty()) { tabOut(); out.write('}'); return true; } if (showType || referenced) { out.write(','); newLine(); } return writeMapBody(map.entrySet().iterator()); } private boolean writeMapBody(final Iterator i) throws IOException { final Writer output = out; while (i.hasNext()) { Entry att2value = (Entry) i.next(); Object value = att2value.getValue(); if (writeOptions.isSkipNullFields() && value == null) { continue; } writeJsonUtf8String(output, (String) att2value.getKey()); output.write(":"); writeCollectionElement(value); if (i.hasNext()) { output.write(','); newLine(); } } tabOut(); output.write('}'); return true; } /** * Ensure that all keys within the Map are String instances * @param map Map to inspect that all keys are primitive. This allows the output JSON * to be optimized into {"key1":value1, "key2": value2} format if all the * keys of the Map are Strings. If not, then a Map is written as two * arrays, a @keys array and an @items array. This allows support for Maps * with non-String keys. */ public static boolean ensureJsonPrimitiveKeys(Map map) { for (Object o : map.keySet()) { if (!(o instanceof String)) { return false; } } return true; } /** * Write an element that is contained in some type of Collection or Map. * @param o Collection element to output in JSON format. * @throws IOException if an error occurs writing to the output stream. */ private void writeCollectionElement(Object o) throws IOException { if (o == null) { out.write("null"); } else if (o instanceof Boolean || o instanceof Double) { writePrimitive(o, false); } else if (o instanceof Long) { writePrimitive(o, getWriteOptions().isWriteLongsAsStrings()); } else if (o instanceof String) { // Never do an @ref to a String (they are treated as logical primitives and intern'ed on read) writeJsonUtf8String(out, (String) o); } else if (getWriteOptions().isNeverShowingType() && MetaUtils.isPrimitive(o.getClass())) { // If neverShowType, then force primitives (and primitive wrappers) // to be output with toString() - prevents {"value":6} for example writePrimitive(o, false); } else { writeImpl(o, true); } } private void writeEnumSet(final EnumSet enumSet) throws IOException { out.write('{'); tabIn(); boolean referenced = this.objsReferenced.containsKey(enumSet); if (referenced) { writeId(getId(enumSet)); out.write(','); newLine(); } out.write("\"@enum\":"); Enum> ee = null; if (!enumSet.isEmpty()) { ee = enumSet.iterator().next(); } else { EnumSet> complement = EnumSet.complementOf(enumSet); if (!complement.isEmpty()) { ee = complement.iterator().next(); } } Field elementTypeField = writeOptions.getDeepDeclaredFields(EnumSet.class).get("elementType"); Class elementType = (Class) getValueByReflect(enumSet, elementTypeField); if ( elementType != null) { // nice we got the right to sneak into } else if (ee == null) { elementType = MetaUtils.Dumpty.class; } else { elementType = ee.getClass(); } writeBasicString(out, elementType.getName()); if (!enumSet.isEmpty()) { Collection mapOfFields = writeOptions.getAccessorsForClass(elementType); int enumFieldsCount = mapOfFields.size(); out.write(","); newLine(); writeBasicString(out, "@items"); out.write(":["); if (enumFieldsCount > 2) { newLine(); } boolean firstInSet = true; for (Enum e : enumSet) { if (!firstInSet) { out.write(","); if (enumFieldsCount > 2) { newLine(); } } firstInSet = false; if (enumFieldsCount <= 2) { writeJsonUtf8String(out, e.name()); } else { boolean firstInEntry = true; out.write('{'); for (Accessor f : mapOfFields) { firstInEntry = writeField(e, firstInEntry, f.getUniqueFieldName(), f); } out.write('}'); } } out.write("]"); } tabOut(); out.write('}'); } /** * @param obj Object to be written in JSON format * @param showType boolean true means show the "@type" field, false * eliminates it. Many times the type can be dropped because it can be * inferred from the field or array type. * @param bodyOnly write only the body of the object * @throws IOException if an error occurs writing to the output stream. */ public void writeObject(final Object obj, boolean showType, boolean bodyOnly) throws IOException { if (writeOptions.isNeverShowingType()) { showType = false; } final boolean referenced = this.objsReferenced.containsKey(obj); if (!bodyOnly) { out.write('{'); tabIn(); if (referenced) { writeId(getId(obj)); } if (referenced && showType) { out.write(','); newLine(); } if (showType) { writeType(obj.getClass().getName(), out); } } boolean first = !showType; if (referenced && !showType) { first = false; } Collection accessors = writeOptions.getAccessorsForClass(obj.getClass()); for (final Accessor accessor : accessors) { final String fieldName = accessor.getUniqueFieldName(); first = writeField(obj, first, fieldName, accessor); } if (!bodyOnly) { tabOut(); out.write('}'); } } private Object getValueByReflect(Object obj, Field field) { try { return field.get(obj); } catch (Exception ignored) { return null; } } private boolean writeField(Object obj, boolean first, String fieldName, Accessor accessor) throws IOException { final Class fieldDeclaringClass = accessor.getDeclaringClass(); Object o; // Only here for enumAsObject writing if (Enum.class.isAssignableFrom(fieldDeclaringClass)) { if (!accessor.isPublic() && writeOptions.isEnumPublicFieldsOnly()) { return first; } o = accessor.retrieve(obj); } else { o = accessor.retrieve(obj); } if (writeOptions.isSkipNullFields() && o == null) { // If skip null, skip field and return the same status on first field written indicator return first; } if (!first) { out.write(','); newLine(); } out.write("\""); out.write(fieldName); // Not using slower UTF String writer for field names out.write("\":"); if (o == null) { // don't quote null out.write("null"); return false; } // check to see if type needs to be written. Class type = accessor.getFieldType(); writeImpl(o, isForceType(o.getClass(), type)); return false; } private boolean isForceType(Class objectClass, Class declaredType) { // When no type is written we can check the Object itself not the declaration final boolean writeLongsAsStrings = writeOptions.isWriteLongsAsStrings(); final boolean objectClassIsLongWrittenAsString = (objectClass == Long.class || objectClass == long.class) && writeLongsAsStrings; final boolean declaredClassIsLongWrittenAsString = (declaredType == Long.class || objectClass == long.class) && writeLongsAsStrings; if (Primitives.isNativeJsonType(objectClass) && !objectClassIsLongWrittenAsString) { return false; } if (Primitives.isPrimitive(declaredType) && !declaredClassIsLongWrittenAsString) { return false; } if (writeOptions.isNeverShowingType() && Primitives.isPrimitive(objectClass) && !objectClassIsLongWrittenAsString) { return false; } if (writeOptions.isAlwaysShowingType()) { return true; } if (objectClass == declaredType) { return false; } if (declaredType.isEnum() && declaredType.isAssignableFrom(objectClass)) { Optional> optionalClass = MetaUtils.getClassIfEnum(objectClass); return declaredType != optionalClass.orElse(null); } return true; } public void flush() { try { if (out != null) { out.flush(); } } catch (Exception ignored) { } } public void close() { try { out.close(); } catch (Exception ignore) { } } private String getId(Object o) { if (o instanceof JsonObject) { long id = ((JsonObject) o).id; if (id > 0) { return String.valueOf(id); } } Long id = this.objsReferenced.get(o); return id == null ? null : Long.toString(id); } /** * Writes out a string without special characters. Use for labels, etc. when you know you * will not need extra formattting for UTF-8 or tabs, quotes and newlines in the string * * @param writer Writer to which the UTF-8 string will be written to * @param s String to be written in UTF-8 format on the output stream. * @throws IOException if an error occurs writing to the output stream. */ public static void writeBasicString(final Writer writer, String s) throws IOException { writer.write('\"'); writer.write(s); writer.write('\"'); } private static void writeChar(Writer writer, char c) throws IOException { if (c < ' ') { // Anything less than ASCII space, write either in \\u00xx form, or the special \t, \n, etc. form switch (c) { case '\b': writer.write("\\b"); break; case '\f': writer.write("\\f"); break; case '\n': writer.write("\\n"); break; case '\r': writer.write("\\r"); break; case '\t': writer.write("\\t"); break; default: writer.write(String.format("\\u%04X", (int) c)); break; } } else if (c == '\\' || c == '"') { writer.write('\\'); writer.write(c); } else { // Anything else - write in UTF-8 form (multibyte encoded) (OutputStreamWriter is UTF-8) writer.write(c); } } /** * Write out special characters "\b, \f, \t, \n, \r", as such, backslash as \\ * quote as \" and values less than an ASCII space (20hex) as "\\u00xx" format, * characters in the range of ASCII space to a '~' as ASCII, and anything higher in UTF-8. * * @param output Writer to which the UTF-8 string will be written to * @param s String to be written in UTF-8 format on the output stream. * @throws IOException if an error occurs writing to the output stream. */ public static void writeJsonUtf8String(final Writer output, String s) throws IOException { output.write('\"'); final int len = s.length(); for (int i = 0; i < len; i++) { writeChar(output, s.charAt(i)); } output.write('\"'); } }