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

com.dslplatform.json.DslJson Maven / Gradle / Ivy

There is a newer version: 1.36.0
Show newest version
package com.dslplatform.json;

import org.w3c.dom.Element;

import java.io.*;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.net.InetAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.*;

/**
 * Main DSL-JSON class.
 * Easiest way to use the library is to create an DslJson<Object> instance and reuse it within application.
 * DslJson has optional constructor for specifying default readers/writers.
 * 

* During initialization DslJson will use ServiceLoader API to load registered services. * This is done through `META-INF/services/com.dslplatform.json.CompiledJson` file. *

* DslJson can fallback to another serializer in case when it doesn't know how to handle specific type. * This can be specified by Fallback interface during initialization. *

* If you wish to use compile time databinding @CompiledJson annotation must be specified on the target class * or implicit reference to target class must exists from a class with @CompiledJson annotation. *

* Usage example: *

 *     DslJson<Object> dsl = new DslJson<>();
 *     dsl.serialize(instance, OutputStream);
 *     POJO pojo = dsl.deserialize(POJO.class, InputStream);
 * 
*

* For best performance use serialization API with JsonWriter and byte[] as target. * JsonWriter is reused via thread local variable. When custom JsonWriter's are used, reusing them will yield maximum performance. * JsonWriter can be reused via reset methods. * For best deserialization performance prefer byte[] API instead of InputStream API. * JsonReader is reused via thread local variable. When custom JsonReaders are used, reusing them will yield maximum performance. * JsonReader can be reused via process methods. *

* During deserialization TContext can be used to pass data into deserialized classes. * This is useful when deserializing domain objects which require state or service provider. * For example DSL Platform entities require service locator to be able to perform lazy load. *

* DslJson doesn't have a String or Reader API since it's optimized for processing bytes and streams. * If you wish to process String, use String.getBytes("UTF-8") as argument for DslJson. * Only UTF-8 is supported for encoding and decoding JSON. *

 *     DslJson<Object> dsl = new DslJson<>();
 *     JsonWriter writer = dsl.newWriter();
 *     dsl.serialize(writer, instance);
 *     String json = writer.toString(); //JSON as string - avoid using JSON as Strings whenever possible
 *     byte[] input = json.getBytes("UTF-8");
 *     POJO pojo = dsl.deserialize(POJO.class, input, input.length);
 * 
* * @param used for library specialization. If unsure, use Object */ public class DslJson implements UnknownSerializer, TypeLookup { private static final Charset UTF8 = Charset.forName("UTF-8"); private static final Object unknownValue = new Object(); /** * The context of this instance. * Can be used for library specialization */ @Nullable public final TContext context; @Nullable protected final Fallback fallback; /** * Should properties with default values be omitted from the resulting JSON? * This will leave out nulls, empty collections, zeros and other attributes with default values * which can be reconstructed from schema information */ public final boolean omitDefaults; /** * When object supports array format, eg. [prop1, prop2, prop3] this value must be enabled before * object will be serialized in such a way. Regardless of this value deserialization will support all formats. */ public final boolean allowArrayFormat; protected final StringCache keyCache; protected final StringCache valuesCache; protected final List> writerFactories = new CopyOnWriteArrayList>(); private final int settingsWriters; protected final List> readerFactories = new CopyOnWriteArrayList>(); private final int settingsReaders; protected final List> binderFactories = new CopyOnWriteArrayList>(); private final int settingsBinders; private final JsonReader.ErrorInfo errorInfo; private final JsonReader.DoublePrecision doublePrecision; private final JsonReader.UnknownNumberParsing unknownNumbers; private final int maxNumberDigits; private final int maxStringSize; protected final ThreadLocal localWriter; protected final ThreadLocal localReader; private final ExternalConverterAnalyzer externalConverterAnalyzer; public interface Fallback { void serialize(@Nullable Object instance, OutputStream stream) throws IOException; @Nullable Object deserialize(@Nullable TContext context, Type manifest, byte[] body, int size) throws IOException; @Nullable Object deserialize(@Nullable TContext context, Type manifest, InputStream stream) throws IOException; } public interface ConverterFactory { @Nullable T tryCreate(Type manifest, DslJson dslJson); } /** * Configuration for DslJson options. * By default key cache is enabled. Everything else is not configured. * To load `META-INF/services` call `includeServiceLoader()` * * @param DslJson context */ public static class Settings { private TContext context; private boolean javaSpecifics; private Fallback fallback; private boolean omitDefaults; private boolean allowArrayFormat; private StringCache keyCache = new SimpleStringCache(); private StringCache valuesCache; private int fromServiceLoader; private JsonReader.ErrorInfo errorInfo = JsonReader.ErrorInfo.WITH_STACK_TRACE; private JsonReader.DoublePrecision doublePrecision = JsonReader.DoublePrecision.DEFAULT; private JsonReader.UnknownNumberParsing unknownNumbers = JsonReader.UnknownNumberParsing.LONG_AND_BIGDECIMAL; private int maxNumberDigits = 512; private int maxStringBuffer = 128 * 1024 * 1024; private final List configurations = new ArrayList(); private final List> writerFactories = new ArrayList>(); private final List> readerFactories = new ArrayList>(); private final List> binderFactories = new ArrayList>(); private final Set classLoaders = new HashSet(); /** * Pass in context for DslJson. * Context will be available in JsonReader for objects which needs it. * * @param context context propagated to JsonReaders * @return itself */ public Settings withContext(@Nullable TContext context) { this.context = context; return this; } /** * Enable converters for Java specific types (Graphics API) not available on Android. * * @param javaSpecifics should register Java specific converters * @return itself */ public Settings withJavaConverters(boolean javaSpecifics) { this.javaSpecifics = javaSpecifics; return this; } /** * Will be eventually replaced with writer/reader factories. * Used by DslJson to call into when trying to serialize/deserialize object which is not supported. * * @param fallback how to handle unsupported type * @return which fallback to use in case of unsupported type */ @Deprecated public Settings fallbackTo(@Nullable Fallback fallback) { this.fallback = fallback; return this; } /** * DslJson can exclude some properties from resulting JSON which it can reconstruct fully from schema information. * Eg. int with value 0 can be omitted since that is default value for the type. * Null values can be excluded since they are handled the same way as missing property. * * @param omitDefaults should exclude default values from resulting JSON * @return itself */ public Settings skipDefaultValues(boolean omitDefaults) { this.omitDefaults = omitDefaults; return this; } /** * Some encoders/decoders support writing objects in array format. * For encoder to write objects in such format, Array format must be defined before the Default and minified formats * and array format must be allowed via this setting. * If objects support multiple formats decoding will work regardless of this setting. * * @param allowArrayFormat allow serialization via array format * @return itself */ public Settings allowArrayFormat(boolean allowArrayFormat) { this.allowArrayFormat = allowArrayFormat; return this; } /** * Use specific key cache implementation. * Key cache is enabled by default and it's used when deserializing unstructured objects such as Map<String, Object> * to avoid allocating new String key instance. Instead StringCache will provide a new or an old instance. * This improves memory usage and performance since there is usually small number of keys. * It does have some performance overhead, but this is dependant on the implementation. *

* To disable key cache, provide null for it. * * @param keyCache which key cache to use * @return itself */ public Settings useKeyCache(@Nullable StringCache keyCache) { this.keyCache = keyCache; return this; } /** * Use specific string values cache implementation. * By default string values cache is disabled. *

* To support memory restricted scenarios where there is limited number of string values, * values cache can be used. *

* Not every "JSON string" will use this cache... eg UUID, LocalDate don't create an instance of string * and therefore don't use this cache. * * @param valuesCache which values cache to use * @return itself */ public Settings useStringValuesCache(@Nullable StringCache valuesCache) { this.valuesCache = valuesCache; return this; } /** * DslJson will iterate over converter factories when requested type is unknown. * Registering writer converter factory allows for constructing JSON converter lazily. * * @param writer registered writer factory * @return itself */ @SuppressWarnings("unchecked") public Settings resolveWriter(ConverterFactory writer) { if (writer == null) throw new IllegalArgumentException("writer can't be null"); if (writerFactories.contains(writer)) { throw new IllegalArgumentException("writer already registered"); } writerFactories.add((ConverterFactory) writer); return this; } /** * DslJson will iterate over converter factories when requested type is unknown. * Registering reader converter factory allows for constructing JSON converter lazily. * * @param reader registered reader factory * @return itself */ @SuppressWarnings("unchecked") public Settings resolveReader(ConverterFactory reader) { if (reader == null) throw new IllegalArgumentException("reader can't be null"); if (readerFactories.contains(reader)) { throw new IllegalArgumentException("reader already registered"); } readerFactories.add((ConverterFactory) reader); return this; } /** * DslJson will iterate over converter factories when requested type is unknown. * Registering binder converter factory allows for constructing JSON converter lazily. * * @param binder registered binder factory * @return itself */ @SuppressWarnings("unchecked") public Settings resolveBinder(ConverterFactory binder) { if (binder == null) throw new IllegalArgumentException("binder can't be null"); if (binderFactories.contains(binder)) { throw new IllegalArgumentException("binder already registered"); } binderFactories.add((ConverterFactory) binder); return this; } /** * Load converters using thread local ClassLoader. * Will scan through `META-INF/services/com.dslplatform.json.Configuration` file and register implementation during startup. * This will pick up compile time databindings if they are available in specific folder. *

* Note that gradle on Android has issues with preserving that file, in which case it can be provided manually. * DslJson will fall back to "expected" class name if it doesn't find anything during scanning. * * @return itself */ public Settings includeServiceLoader() { return includeServiceLoader(Thread.currentThread().getContextClassLoader()); } /** * Load converters using provided `ClassLoader` instance * Will scan through `META-INF/services/com.dslplatform.json.Configuration` file and register implementation during startup. * This will pick up compile time databindings if they are available in specific folder. *

* Note that gradle on Android has issues with preserving that file, in which case it can be provided manually. * DslJson will fall back to "expected" class name if it doesn't find anything during scanning. * * @param loader ClassLoader to use * @return itself */ public Settings includeServiceLoader(ClassLoader loader) { if (loader == null) throw new IllegalArgumentException("loader can't be null"); classLoaders.add(loader); for (Configuration c : ServiceLoader.load(Configuration.class, loader)) { boolean hasConfiguration = false; Class manifest = c.getClass(); for (Configuration cur : configurations) { if (cur.getClass() == manifest) { hasConfiguration = true; break; } } if (!hasConfiguration) { fromServiceLoader++; configurations.add(c); } } return this; } /** * By default doubles are not deserialized into an exact value in some rare edge cases. * * @param errorInfo information about error in parsing exception * @return itself */ public Settings errorInfo(JsonReader.ErrorInfo errorInfo) { if (errorInfo == null) throw new IllegalArgumentException("errorInfo can't be null"); this.errorInfo = errorInfo; return this; } /** * By default doubles are not deserialized into an exact value in some rare edge cases. * * @param precision type of double deserialization * @return itself */ public Settings doublePrecision(JsonReader.DoublePrecision precision) { if (precision == null) throw new IllegalArgumentException("precision can't be null"); this.doublePrecision = precision; return this; } /** * When processing JSON without a schema numbers can be deserialized in various ways: * * - as longs and decimals * - as longs and doubles * - as decimals only * - as doubles only * * Default is as long and BigDecimal * * @param unknownNumbers how to deserialize numbers without a schema * @return itself */ public Settings unknownNumbers(JsonReader.UnknownNumberParsing unknownNumbers) { if (unknownNumbers == null) throw new IllegalArgumentException("unknownNumbers can't be null"); this.unknownNumbers = unknownNumbers; return this; } /** * Specify maximum allowed size for digits buffer. Default is 512. * Digits buffer is used when processing strange/large input numbers. * * @param size maximum allowed size for digit buffer * @return itself */ public Settings limitDigitsBuffer(int size) { if (size < 1) throw new IllegalArgumentException("size can't be smaller than 1"); this.maxNumberDigits = size; return this; } /** * Specify maximum allowed size for string buffer. Default is 128MB * To protect against malicious inputs, maximum allowed string buffer can be reduced. * * @param size maximum size of buffer in bytes * @return itself */ public Settings limitStringBuffer(int size) { if (size < 1) throw new IllegalArgumentException("size can't be smaller than 1"); this.maxStringBuffer = size; return this; } /** * Configure DslJson with custom Configuration during startup. * Configurations are extension points for setting up readers/writers during DslJson initialization. * * @param conf custom extensibility point * @return itself */ public Settings with(Configuration conf) { if (conf == null) throw new IllegalArgumentException("conf can't be null"); configurations.add(conf); return this; } private Settings with(Iterable confs) { if (confs != null) { for (Configuration c : confs) configurations.add(c); } return this; } } /** * Simple initialization entry point. * Will provide null for TContext * Java graphics readers/writers will not be registered. * Fallback will not be configured. * Key cache will be enables, values cache will be disabled. * Default ServiceLoader.load method will be used to setup services from META-INF */ public DslJson() { this(new Settings().includeServiceLoader()); } /** * Will be removed. Use DslJson(Settings) instead. * Fully configurable entry point. * * @param context context instance which can be provided to deserialized objects. Use null if not sure * @param javaSpecifics register Java graphics specific classes such as java.awt.Point, Image, ... * @param fallback in case of unsupported type, try serialization/deserialization through external API * @param omitDefaults should serialization produce minified JSON (omit nulls and default values) * @param keyCache parsed keys can be cached (this is only used in small subset of parsing) * @param serializers additional serializers/deserializers which will be immediately registered into readers/writers */ @Deprecated public DslJson( @Nullable final TContext context, final boolean javaSpecifics, @Nullable final Fallback fallback, final boolean omitDefaults, @Nullable final StringCache keyCache, final Iterable serializers) { this(new Settings() .withContext(context) .withJavaConverters(javaSpecifics) .fallbackTo(fallback) .skipDefaultValues(omitDefaults) .useKeyCache(keyCache) .with(serializers) ); } /** * Fully configurable entry point. * Provide settings for DSL-JSON initialization. * * @param settings DSL-JSON configuration */ public DslJson(final Settings settings) { if (settings == null) throw new IllegalArgumentException("settings can't be null"); final DslJson self = this; this.localWriter = new ThreadLocal() { @Override protected JsonWriter initialValue() { return new JsonWriter(4096, self); } }; this.localReader = new ThreadLocal() { @Override protected JsonReader initialValue() { return new JsonReader(new byte[4096], 4096, self.context, new char[64], self.keyCache, self.valuesCache, self, self.errorInfo, self.doublePrecision, self.unknownNumbers, self.maxNumberDigits, self.maxStringSize); } }; this.context = settings.context; this.fallback = settings.fallback; this.omitDefaults = settings.omitDefaults; this.allowArrayFormat = settings.allowArrayFormat; this.keyCache = settings.keyCache; this.valuesCache = settings.valuesCache; this.unknownNumbers = settings.unknownNumbers; this.errorInfo = settings.errorInfo; this.doublePrecision = settings.doublePrecision; this.maxNumberDigits = settings.maxNumberDigits; this.maxStringSize = settings.maxStringBuffer; this.writerFactories.addAll(settings.writerFactories); this.settingsWriters = settings.writerFactories.size(); this.readerFactories.addAll(settings.readerFactories); this.settingsReaders = settings.readerFactories.size(); this.binderFactories.addAll(settings.binderFactories); this.settingsBinders = settings.binderFactories.size(); this.externalConverterAnalyzer = new ExternalConverterAnalyzer(settings.classLoaders); registerReader(byte[].class, BinaryConverter.Base64Reader); registerWriter(byte[].class, BinaryConverter.Base64Writer); registerReader(boolean.class, BoolConverter.READER); registerWriter(boolean.class, BoolConverter.WRITER); registerDefault(boolean.class, false); registerReader(boolean[].class, BoolConverter.ARRAY_READER); registerWriter(boolean[].class, BoolConverter.ARRAY_WRITER); registerReader(Boolean.class, BoolConverter.NULLABLE_READER); registerWriter(Boolean.class, BoolConverter.WRITER); if (settings.javaSpecifics) { registerJavaSpecifics(this); } registerReader(LinkedHashMap.class, ObjectConverter.MapReader); registerReader(HashMap.class, ObjectConverter.MapReader); registerReader(Map.class, ObjectConverter.MapReader); registerWriter(Map.class, new JsonWriter.WriteObject() { @Override public void write(JsonWriter writer, @Nullable Map value) { if (value == null) { writer.writeNull(); } else { try { serializeMap(value, writer); } catch (IOException ex) { throw new SerializationException(ex); } } } }); registerReader(URI.class, NetConverter.UriReader); registerWriter(URI.class, NetConverter.UriWriter); registerReader(InetAddress.class, NetConverter.AddressReader); registerWriter(InetAddress.class, NetConverter.AddressWriter); registerReader(double.class, NumberConverter.DOUBLE_READER); registerWriter(double.class, NumberConverter.DOUBLE_WRITER); registerDefault(double.class, 0.0); registerReader(double[].class, NumberConverter.DOUBLE_ARRAY_READER); registerWriter(double[].class, NumberConverter.DOUBLE_ARRAY_WRITER); registerReader(Double.class, NumberConverter.NULLABLE_DOUBLE_READER); registerWriter(Double.class, NumberConverter.DOUBLE_WRITER); registerReader(float.class, NumberConverter.FLOAT_READER); registerWriter(float.class, NumberConverter.FLOAT_WRITER); registerDefault(float.class, 0.0f); registerReader(float[].class, NumberConverter.FLOAT_ARRAY_READER); registerWriter(float[].class, NumberConverter.FLOAT_ARRAY_WRITER); registerReader(Float.class, NumberConverter.NULLABLE_FLOAT_READER); registerWriter(Float.class, NumberConverter.FLOAT_WRITER); registerReader(int.class, NumberConverter.INT_READER); registerWriter(int.class, NumberConverter.INT_WRITER); registerDefault(int.class, 0); registerReader(int[].class, NumberConverter.INT_ARRAY_READER); registerWriter(int[].class, NumberConverter.INT_ARRAY_WRITER); registerReader(Integer.class, NumberConverter.NULLABLE_INT_READER); registerWriter(Integer.class, NumberConverter.INT_WRITER); registerReader(short.class, NumberConverter.SHORT_READER); registerWriter(short.class, NumberConverter.SHORT_WRITER); registerDefault(short.class, (short)0); registerReader(short[].class, NumberConverter.SHORT_ARRAY_READER); registerWriter(short[].class, NumberConverter.SHORT_ARRAY_WRITER); registerReader(Short.class, NumberConverter.NULLABLE_SHORT_READER); registerWriter(Short.class, NumberConverter.SHORT_WRITER); registerReader(long.class, NumberConverter.LONG_READER); registerWriter(long.class, NumberConverter.LONG_WRITER); registerDefault(long.class, 0L); registerReader(long[].class, NumberConverter.LONG_ARRAY_READER); registerWriter(long[].class, NumberConverter.LONG_ARRAY_WRITER); registerReader(Long.class, NumberConverter.NULLABLE_LONG_READER); registerWriter(Long.class, NumberConverter.LONG_WRITER); registerReader(BigDecimal.class, NumberConverter.DecimalReader); registerWriter(BigDecimal.class, NumberConverter.DecimalWriter); registerReader(String.class, StringConverter.READER); registerWriter(String.class, StringConverter.WRITER); registerReader(UUID.class, UUIDConverter.READER); registerWriter(UUID.class, UUIDConverter.WRITER); registerReader(Number.class, NumberConverter.NumberReader); registerWriter(CharSequence.class, StringConverter.WRITER_CHARS); registerReader(StringBuilder.class, StringConverter.READER_BUILDER); registerReader(StringBuffer.class, StringConverter.READER_BUFFER); for (Configuration serializer : settings.configurations) { serializer.configure(this); } if (!settings.classLoaders.isEmpty() && settings.fromServiceLoader == 0) { //TODO: workaround common issue with failed services registration. try to load common external name if exists loadDefaultConverters(this, settings.classLoaders, "dsl_json_Annotation_Processor_External_Serialization"); loadDefaultConverters(this, settings.classLoaders, "dsl_json.json.ExternalSerialization"); loadDefaultConverters(this, settings.classLoaders, "dsl_json_ExternalSerialization"); } } /** * Simplistic string cache implementation. * It uses a fixed String[] structure in which it caches string value based on it's hash. * Eg, hash & mask provide index into the structure. Different string with same hash will overwrite the previous one. */ public static class SimpleStringCache implements StringCache { private final int mask; private final String[] cache; /** * Will use String[] with 1024 elements. */ public SimpleStringCache() { this(10); } public SimpleStringCache(int log2Size) { int size = 2; for (int i = 1; i < log2Size; i++) { size *= 2; } mask = size - 1; cache = new String[size]; } /** * Calculates hash of the provided "string" and looks it up from the String[] * It it doesn't exists of a different string is already there a new String instance is created * and saved into the String[] * * @param chars buffer into which string was parsed * @param len the string length inside the buffer * @return String instance matching the char[]/int pair */ @Override public String get(char[] chars, int len) { long hash = 0x811c9dc5; for (int i = 0; i < len; i++) { hash ^= (byte) chars[i]; hash *= 0x1000193; } final int index = (int) hash & mask; final String value = cache[index]; if (value == null) return createAndPut(index, chars, len); if (value.length() != len) return createAndPut(index, chars, len); for (int i = 0; i < value.length(); i++) { if (value.charAt(i) != chars[i]) return createAndPut(index, chars, len); } return value; } private String createAndPut(int index, char[] chars, int len) { final String value = new String(chars, 0, len); cache[index] = value; return value; } } /** * Create a writer bound to this DSL-JSON. * Ideally it should be reused. * Bound writer can use lookups to find custom writers. * This can be used to serialize unknown types such as Object.class * * @return bound writer */ public JsonWriter newWriter() { return new JsonWriter(this); } /** * Create a writer bound to this DSL-JSON. * Ideally it should be reused. * Bound writer can use lookups to find custom writers. * This can be used to serialize unknown types such as Object.class * * @param size initial buffer size * @return bound writer */ public JsonWriter newWriter(int size) { return new JsonWriter(size, this); } /** * Create a writer bound to this DSL-JSON. * Ideally it should be reused. * Bound writer can use lookups to find custom writers. * This can be used to serialize unknown types such as Object.class * * @param buffer initial buffer * @return bound writer */ public JsonWriter newWriter(byte[] buffer) { if (buffer == null) throw new IllegalArgumentException("null value provided for buffer"); return new JsonWriter(buffer, this); } /** * Create a reader bound to this DSL-JSON. * Bound reader can reuse key cache (which is used during Map deserialization) * This reader can be reused via process method. * * @return bound reader */ public JsonReader newReader() { return new JsonReader(new byte[4096], 4096, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); } /** * Create a reader bound to this DSL-JSON. * Bound reader can reuse key cache (which is used during Map deserialization) * This reader can be reused via process method. * * @param bytes input bytes * @return bound reader */ public JsonReader newReader(byte[] bytes) { return new JsonReader(bytes, bytes.length, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); } /** * Create a reader bound to this DSL-JSON. * Bound reader can reuse key cache (which is used during Map deserialization) * This reader can be reused via process method. * * @param bytes input bytes * @param length use input bytes up to specified length * @return bound reader */ public JsonReader newReader(byte[] bytes, int length) { return new JsonReader(bytes, length, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); } /** * Create a reader bound to this DSL-JSON. * Bound reader can reuse key cache (which is used during Map deserialization) * Pass in initial string buffer. * This reader can be reused via process method. * * @param bytes input bytes * @param length use input bytes up to specified length * @param tmp string parsing buffer * @return bound reader */ public JsonReader newReader(byte[] bytes, int length, char[] tmp) { return new JsonReader(bytes, length, context, tmp, keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); } /** * Create a reader bound to this DSL-JSON. * Bound reader can reuse key cache (which is used during Map deserialization) * Created reader can be reused (using process method). * This is convenience method for creating a new reader and binding it to stream. * * @param stream input stream * @param buffer temporary buffer * @return bound reader * @throws java.io.IOException unable to read from stream */ public JsonReader newReader(InputStream stream, byte[] buffer) throws IOException { final JsonReader reader = newReader(buffer); reader.process(stream); return reader; } /** * Create a reader bound to this DSL-JSON. * Bound reader can reuse key cache (which is used during Map deserialization) * This method id Deprecated since it should be avoided. * It's better to use byte[] or InputStream based readers * * @param input JSON string * @return bound reader */ @Deprecated public JsonReader newReader(String input) { final byte[] bytes = input.getBytes(UTF8); return new JsonReader(bytes, bytes.length, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); } private static void loadDefaultConverters(final DslJson json, Set loaders, final String name) { for (ClassLoader loader : loaders) { try { Class external = loader.loadClass(name); Configuration instance = (Configuration) external.newInstance(); instance.configure(json); } catch (NoClassDefFoundError ignore) { } catch (Exception ignore) { } } } static void registerJavaSpecifics(final DslJson json) { json.registerReader(java.awt.geom.Point2D.Double.class, JavaGeomConverter.LocationReader); json.registerReader(java.awt.geom.Point2D.class, JavaGeomConverter.LocationReader); json.registerWriter(java.awt.geom.Point2D.class, JavaGeomConverter.LocationWriter); json.registerReader(java.awt.Point.class, JavaGeomConverter.PointReader); json.registerWriter(java.awt.Point.class, JavaGeomConverter.PointWriter); json.registerReader(java.awt.geom.Rectangle2D.Double.class, JavaGeomConverter.RectangleReader); json.registerReader(java.awt.geom.Rectangle2D.class, JavaGeomConverter.RectangleReader); json.registerWriter(java.awt.geom.Rectangle2D.class, JavaGeomConverter.RectangleWriter); json.registerReader(java.awt.image.BufferedImage.class, JavaGeomConverter.ImageReader); json.registerReader(java.awt.Image.class, JavaGeomConverter.ImageReader); json.registerWriter(java.awt.Image.class, JavaGeomConverter.ImageWriter); json.registerReader(Element.class, XmlConverter.Reader); json.registerWriter(Element.class, XmlConverter.Writer); } private final Map defaults = new ConcurrentHashMap(); public void registerDefault(Class manifest, T instance) { defaults.put(manifest, instance); } @SuppressWarnings("unchecked") public boolean registerWriterFactory(ConverterFactory factory) { if (factory == null) throw new IllegalArgumentException("factory can't be null"); if (writerFactories.contains(factory)) return false; writerFactories.add(writerFactories.size() - settingsWriters, (ConverterFactory) factory); return true; } @SuppressWarnings("unchecked") public boolean registerReaderFactory(ConverterFactory factory) { if (factory == null) throw new IllegalArgumentException("factory can't be null"); if (readerFactories.contains(factory)) return false; readerFactories.add(readerFactories.size() - settingsReaders, (ConverterFactory) factory); return true; } @SuppressWarnings("unchecked") public boolean registerBinderFactory(ConverterFactory factory) { if (factory == null) throw new IllegalArgumentException("factory can't be null"); if (binderFactories.contains(factory)) return false; binderFactories.add(binderFactories.size() - settingsBinders, (ConverterFactory) factory); return true; } @Nullable public final Object getDefault(@Nullable Type manifest) { if (manifest == null) return null; Object instance = defaults.get(manifest); if (instance != null) return instance; final Class rawType; if (manifest instanceof Class) { rawType = (Class) manifest; } else if (manifest instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) manifest; rawType = (Class) pt.getRawType(); } else return null; if (rawType.isPrimitive()) { return Array.get(Array.newInstance(rawType, 1), 0); } return defaults.get(rawType); } private final ConcurrentMap, JsonReader.ReadJsonObject> objectReaders = new ConcurrentHashMap, JsonReader.ReadJsonObject>(); private final ConcurrentMap readers = new ConcurrentHashMap(); private final ConcurrentMap binders = new ConcurrentHashMap(); private final ConcurrentMap writers = new ConcurrentHashMap(); public final Set getRegisteredDecoders() { return readers.keySet(); } public final Set getRegisteredBinders() { return binders.keySet(); } public final Set getRegisteredEncoders() { return writers.keySet(); } /** * Register custom reader for specific type (JSON -> instance conversion). * Reader is used for conversion from input byte[] -> target object instance *

* Types registered through @CompiledJson annotation should be registered automatically through * ServiceLoader.load method and you should not be registering them manually. *

* If null is registered for a reader this will disable deserialization of specified type * * @param manifest specified type * @param reader provide custom implementation for reading JSON into an object instance * @param type * @param type or subtype */ public void registerReader(final Class manifest, @Nullable final JsonReader.ReadObject reader) { if (reader == null) readers.remove(manifest); else readers.put(manifest, reader); } /** * Register custom reader for specific type (JSON -> instance conversion). * Reader is used for conversion from input byte[] -> target object instance *

* Types registered through @CompiledJson annotation should be registered automatically through * ServiceLoader.load method and you should not be registering them manually. *

* If null is registered for a reader this will disable deserialization of specified type * * @param manifest specified type * @param reader provide custom implementation for reading JSON into an object instance * @return old registered value */ @Nullable public JsonReader.ReadObject registerReader(final Type manifest, @Nullable final JsonReader.ReadObject reader) { if (reader == null) return readers.remove(manifest); try { return readers.get(manifest); } finally { readers.put(manifest, reader); } } /** * Register custom binder for specific type (JSON -> instance conversion). * Binder is used for conversion from input byte[] -> existing target object instance. * It's similar to reader, with the difference that it accepts target instance. *

* Types registered through @CompiledJson annotation should be registered automatically through * ServiceLoader.load method and you should not be registering them manually. *

* If null is registered for a binder this will disable binding of specified type * * @param manifest specified type * @param binder provide custom implementation for binding JSON to an object instance * @param type * @param type or subtype */ public void registerBinder(final Class manifest, @Nullable final JsonReader.BindObject binder) { if (binder == null) binders.remove(manifest); else binders.put(manifest, binder); } /** * Register custom binder for specific type (JSON -> instance conversion). * Binder is used for conversion from input byte[] -> existing target object instance. * It's similar to reader, with the difference that it accepts target instance. *

* Types registered through @CompiledJson annotation should be registered automatically through * ServiceLoader.load method and you should not be registering them manually. *

* If null is registered for a binder this will disable binding of specified type * * @param manifest specified type * @param binder provide custom implementation for binding JSON to an object instance */ public void registerBinder(final Type manifest, @Nullable final JsonReader.BindObject binder) { if (binder == null) binders.remove(manifest); else binders.put(manifest, binder); } /** * Register custom writer for specific type (instance -> JSON conversion). * Writer is used for conversion from object instance -> output byte[] *

* Types registered through @CompiledJson annotation should be registered automatically through * ServiceLoader.load method and you should not be registering them manually. *

* If null is registered for a writer this will disable serialization of specified type * * @param manifest specified type * @param writer provide custom implementation for writing JSON from object instance * @param type */ public void registerWriter(final Class manifest, @Nullable final JsonWriter.WriteObject writer) { if (writer == null) { writerMap.remove(manifest); writers.remove(manifest); } else { writerMap.put(manifest, manifest); writers.put(manifest, writer); } } /** * Register custom writer for specific type (instance -> JSON conversion). * Writer is used for conversion from object instance -> output byte[] *

* Types registered through @CompiledJson annotation should be registered automatically through * ServiceLoader.load method and you should not be registering them manually. *

* If null is registered for a writer this will disable serialization of specified type * * @param manifest specified type * @param writer provide custom implementation for writing JSON from object instance * @return old registered value */ @Nullable public JsonWriter.WriteObject registerWriter(final Type manifest, @Nullable final JsonWriter.WriteObject writer) { if (writer == null) return writers.remove(manifest); try { return writers.get(manifest); } finally { writers.put(manifest, writer); } } private final ConcurrentMap, Class> writerMap = new ConcurrentHashMap, Class>(); /** * Try to find registered writer for provided type. * If writer is not found, null will be returned. * If writer for exact type is not found, type hierarchy will be scanned for base writer. *

* Writer is used for conversion from object instance into JSON representation. * * @param manifest specified type * @return writer for specified type if found */ @Nullable public JsonWriter.WriteObject tryFindWriter(final Type manifest) { JsonWriter.WriteObject writer = writers.get(manifest); if (writer != null) return writer; final Type actualType = extractActualType(manifest); if (actualType != manifest) { writer = writers.get(actualType); if (writer != null) { writers.putIfAbsent(manifest, writer); return writer; } } if (actualType instanceof Class) { final Class signature = (Class) actualType; if (JsonObject.class.isAssignableFrom(signature)) { writers.putIfAbsent(manifest, OBJECT_WRITER); return OBJECT_WRITER; } } writer = lookupFromFactories(manifest, actualType, writerFactories, writers); if (writer != null) return writer; if (!(actualType instanceof Class)) return null; Class found = writerMap.get(actualType); if (found != null) { return writers.get(found); } Class container = (Class) actualType; final ArrayList> signatures = new ArrayList>(); findAllSignatures(container, signatures); for (final Class sig : signatures) { writer = writers.get(sig); if (writer == null) { writer = lookupFromFactories(manifest, sig, writerFactories, writers); } if (writer != null) { writerMap.putIfAbsent(container, sig); return writer; } } return null; } private static Type extractActualType(final Type manifest) { if (manifest instanceof WildcardType) { WildcardType wt = (WildcardType) manifest; if (wt.getUpperBounds().length == 1 && wt.getLowerBounds().length == 0) { return wt.getUpperBounds()[0]; } } return manifest; } private void checkExternal(final Type manifest, final ConcurrentMap cache) { if (manifest instanceof Class) { externalConverterAnalyzer.tryFindConverter((Class) manifest, this); } else if (manifest instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) manifest; Type container = pt.getRawType(); externalConverterAnalyzer.tryFindConverter((Class) container, this); for (Type arg : pt.getActualTypeArguments()) { if (!cache.containsKey(arg)) { Type actualType = extractActualType(arg); if (actualType != arg && !cache.containsKey(actualType)) { checkExternal(actualType, cache); } } } } } @Nullable private T lookupFromFactories( final Type signature, final Type manifest, final List> factories, final ConcurrentMap cache) { if (manifest instanceof Class) { externalConverterAnalyzer.tryFindConverter((Class) manifest, this); T found = cache.get(manifest); if (found != null) return found; } else if (manifest instanceof ParameterizedType) { checkExternal(manifest, cache); } for (ConverterFactory wrt : factories) { final T converter = wrt.tryCreate(manifest, this); if (converter != null) { cache.putIfAbsent(signature, converter); return converter; } } return null; } /** * Try to find registered reader for provided type. * If reader is not found, null will be returned. * Exact match must be found, type hierarchy will not be scanned for alternative readers. *

* If you wish to use alternative reader for specific type, register it manually with something along the lines of *

	 *     DslJson dslJson = ...
	 *     dslJson.registerReader(Interface.class, dslJson.tryFindReader(Implementation.class));
	 * 
* * @param manifest specified type * @return found reader for specified type */ @Nullable public JsonReader.ReadObject tryFindReader(final Type manifest) { JsonReader.ReadObject found = readers.get(manifest); if (found != null) return found; final Type actualType = extractActualType(manifest); if (actualType != manifest) { found = readers.get(actualType); if (found != null) { readers.putIfAbsent(manifest, found); return found; } } if (actualType instanceof Class) { final Class signature = (Class) actualType; if (JsonObject.class.isAssignableFrom(signature)) { final JsonReader.ReadJsonObject decoder = getObjectReader(signature); if (decoder != null) { found = convertToReader(decoder); readers.putIfAbsent(manifest, found); return found; } } } return lookupFromFactories(manifest, actualType, readerFactories, readers); } /** * Try to find registered binder for provided type. * If binder is not found, null will be returned. * Exact match must be found, type hierarchy will not be scanned for alternative binders. *

* If you wish to use alternative binder for specific type, register it manually with something along the lines of *

	 *     DslJson dslJson = ...
	 *     dslJson.registerBinder(Interface.class, dslJson.tryFindBinder(Implementation.class));
	 * 
* * @param manifest specified type * @return found reader for specified type */ @Nullable public JsonReader.BindObject tryFindBinder(final Type manifest) { JsonReader.BindObject found = binders.get(manifest); if (found != null) return found; final Type actualType = extractActualType(manifest); if (actualType != manifest) { found = binders.get(actualType); if (found != null) { binders.putIfAbsent(manifest, found); return found; } } return lookupFromFactories(manifest, actualType, binderFactories, binders); } /** * Try to find registered writer for provided type. * If writer is not found, null will be returned. * If writer for exact type is not found, type hierarchy will be scanned for base writer. *

* Writer is used for conversion from object instance into JSON representation. * * @param manifest specified class * @param specified type * @return found writer for specified class or null */ @SuppressWarnings("unchecked") @Nullable public JsonWriter.WriteObject tryFindWriter(final Class manifest) { return (JsonWriter.WriteObject) tryFindWriter((Type) manifest); } /** * Try to find registered reader for provided type. * If reader is not found, null will be returned. * Exact match must be found, type hierarchy will not be scanned for alternative reader. *

* If you wish to use alternative reader for specific type, register it manually with something along the lines of *

	 *     DslJson dslJson = ...
	 *     dslJson.registerReader(Interface.class, dslJson.tryFindReader(Implementation.class));
	 * 
* * @param manifest specified class * @param specified type * @return found reader for specified class or null */ @SuppressWarnings("unchecked") @Nullable public JsonReader.ReadObject tryFindReader(final Class manifest) { return (JsonReader.ReadObject) tryFindReader((Type) manifest); } /** * Try to find registered binder for provided type. * If binder is not found, null will be returned. * Exact match must be found, type hierarchy will not be scanned for alternative binder. *

* If you wish to use alternative binder for specific type, register it manually with something along the lines of *

	 *     DslJson dslJson = ...
	 *     dslJson.registerBinder(Interface.class, dslJson.tryFindBinder(Implementation.class));
	 * 
* * @param manifest specified class * @param specified type * @return found reader for specified class or null */ @SuppressWarnings("unchecked") @Nullable public JsonReader.BindObject tryFindBinder(final Class manifest) { return (JsonReader.BindObject) tryFindBinder((Type) manifest); } private static void findAllSignatures(final Class manifest, final ArrayList> found) { if (found.contains(manifest)) { return; } found.add(manifest); final Class superClass = manifest.getSuperclass(); if (superClass != null && superClass != Object.class) { findAllSignatures(superClass, found); } for (final Class iface : manifest.getInterfaces()) { findAllSignatures(iface, found); } } @SuppressWarnings("unchecked") @Nullable private JsonReader.ReadJsonObject probeForObjectReader(Class manifest, Object instance) { Object found; try { found = manifest.getField("JSON_READER").get(instance); } catch (Exception ignore) { try { found = manifest.getMethod("JSON_READER").invoke(instance); } catch (Exception ignore2) { try { found = manifest.getMethod("getJSON_READER").invoke(instance); } catch (Exception ignore3) { return null; } } } return found instanceof JsonReader.ReadJsonObject ? (JsonReader.ReadJsonObject)found : null; } @SuppressWarnings("unchecked") @Nullable protected final JsonReader.ReadJsonObject getObjectReader(final Class manifest) { try { JsonReader.ReadJsonObject reader = objectReaders.get(manifest); if (reader == null) { reader = probeForObjectReader(manifest, null); if (reader == null) { //probe in few special places try { Object companion = manifest.getField("Companion").get(null); reader = probeForObjectReader(companion.getClass(), companion); } catch (Exception ignore) { return null; } } if (reader != null) { objectReaders.putIfAbsent(manifest, reader); } } return reader; } catch (final Exception ignore) { return null; } } public void serializeMap(final Map value, final JsonWriter sw) throws IOException { sw.writeByte(JsonWriter.OBJECT_START); final int size = value.size(); if (size > 0) { final Iterator> iterator = value.entrySet().iterator(); Map.Entry kv = iterator.next(); sw.writeString(kv.getKey()); sw.writeByte(JsonWriter.SEMI); serialize(sw, kv.getValue()); for (int i = 1; i < size; i++) { sw.writeByte(JsonWriter.COMMA); kv = iterator.next(); sw.writeString(kv.getKey()); sw.writeByte(JsonWriter.SEMI); serialize(sw, kv.getValue()); } } sw.writeByte(JsonWriter.OBJECT_END); } @Deprecated @Nullable public static Object deserializeObject(final JsonReader reader) throws IOException { return ObjectConverter.deserializeObject(reader); } /** * Will be removed * @param reader JSON reader * @return deseralized list * @throws IOException error during parsing */ @Deprecated public static ArrayList deserializeList(final JsonReader reader) throws IOException { return ObjectConverter.deserializeList(reader); } /** * Will be removed * @param reader JSON reader * @return deserialized map * @throws IOException error during parsing */ @Deprecated public static LinkedHashMap deserializeMap(final JsonReader reader) throws IOException { return ObjectConverter.deserializeMap(reader); } private static Object convertResultToArray(Class elementType, List result) { if (elementType.isPrimitive()) { if (boolean.class.equals(elementType)) { boolean[] array = new boolean[result.size()]; for (int i = 0; i < result.size(); i++) { array[i] = (Boolean) result.get(i); } return array; } else if (int.class.equals(elementType)) { int[] array = new int[result.size()]; for (int i = 0; i < result.size(); i++) { array[i] = (Integer) result.get(i); } return array; } else if (long.class.equals(elementType)) { long[] array = new long[result.size()]; for (int i = 0; i < result.size(); i++) { array[i] = (Long) result.get(i); } return array; } else if (short.class.equals(elementType)) { short[] array = new short[result.size()]; for (int i = 0; i < result.size(); i++) { array[i] = (Short) result.get(i); } return array; } else if (byte.class.equals(elementType)) { byte[] array = new byte[result.size()]; for (int i = 0; i < result.size(); i++) { array[i] = (Byte) result.get(i); } return array; } else if (float.class.equals(elementType)) { float[] array = new float[result.size()]; for (int i = 0; i < result.size(); i++) { array[i] = (Float) result.get(i); } return array; } else if (double.class.equals(elementType)) { double[] array = new double[result.size()]; for (int i = 0; i < result.size(); i++) { array[i] = (Double) result.get(i); } return array; } else if (char.class.equals(elementType)) { char[] array = new char[result.size()]; for (int i = 0; i < result.size(); i++) { array[i] = (Character) result.get(i); } return array; } } return result.toArray((Object[]) Array.newInstance(elementType, 0)); } /** * Check if DslJson knows how to serialize a type. * It will check if a writer for such type exists or can be used. * * @param manifest type to check * @return can serialize this type into JSON */ public final boolean canSerialize(final Type manifest) { JsonWriter.WriteObject writer = writers.get(manifest); if (writer != null) return true; if (manifest instanceof Class) { final Class content = (Class) manifest; if (JsonObject.class.isAssignableFrom(content)) { return true; } if (JsonObject[].class.isAssignableFrom(content)) { return true; } if (tryFindWriter(manifest) != null) { return true; } if (content.isArray()) { return !content.getComponentType().isArray() && !Collection.class.isAssignableFrom(content.getComponentType()) && canSerialize(content.getComponentType()); } } if (manifest instanceof ParameterizedType) { final ParameterizedType pt = (ParameterizedType) manifest; if (pt.getActualTypeArguments().length == 1) { final Class container = (Class) pt.getRawType(); if (container.isArray() || Collection.class.isAssignableFrom(container)) { final Type content = pt.getActualTypeArguments()[0]; return content instanceof Class && JsonObject.class.isAssignableFrom((Class) content) || tryFindWriter(content) != null; } } } else if (manifest instanceof GenericArrayType) { final GenericArrayType gat = (GenericArrayType) manifest; return gat.getGenericComponentType() instanceof Class && JsonObject.class.isAssignableFrom((Class) gat.getGenericComponentType()) || tryFindWriter(gat.getGenericComponentType()) != null; } for (ConverterFactory wrt : writerFactories) { if (wrt.tryCreate(manifest, this) != null) { return true; } } return false; } /** * Check if DslJson knows how to deserialize a type. * It will check if a reader for such type exists or can be used. * * @param manifest type to check * @return can read this type from JSON */ public final boolean canDeserialize(final Type manifest) { if (tryFindReader(manifest) != null) { return true; } if (manifest instanceof Class) { final Class objectType = (Class) manifest; if (objectType.isArray()) { return !objectType.getComponentType().isArray() && !Collection.class.isAssignableFrom(objectType.getComponentType()) && canDeserialize(objectType.getComponentType()); } } if (manifest instanceof ParameterizedType) { final ParameterizedType pt = (ParameterizedType) manifest; if (pt.getActualTypeArguments().length == 1) { final Class container = (Class) pt.getRawType(); if (container.isArray() || Collection.class.isAssignableFrom(container)) { final Type content = pt.getActualTypeArguments()[0]; if (tryFindReader(content) != null) { return true; } } } } else if (manifest instanceof GenericArrayType) { final Type content = ((GenericArrayType) manifest).getGenericComponentType(); return tryFindReader(content) != null; } return false; } /** * Reusable deserialize API. * For maximum performance `JsonReader` should be reused (otherwise small buffer will be allocated for processing) * and `JsonReader.ReadObject` should be prepared (otherwise a lookup will be required). *

* This is mostly convenience API since it starts the processing of the JSON by calling getNextToken on JsonReader, * checks for null and calls converter.read(input). * * @param specified type * @param converter target reader * @param input input JSON * @return deserialized instance * @throws IOException error during deserialization */ @Nullable public T deserialize( final JsonReader.ReadObject converter, final JsonReader input) throws IOException { if (converter == null) { throw new IllegalArgumentException("converter can't be null"); } if (input == null) { throw new IllegalArgumentException("input can't be null"); } input.getNextToken(); return converter.read(input); } /** * Convenient deserialize API for working with bytes. * Deserialize provided byte input into target object. *

* Since JSON is often though of as a series of char, * most libraries will convert inputs into a sequence of chars and do processing on them. * DslJson will treat input as a sequence of bytes which allows for various optimizations. * * @param manifest target type * @param body input JSON * @param size length * @param target type * @return deserialized instance * @throws IOException error during deserialization */ @SuppressWarnings("unchecked") @Nullable public TResult deserialize( final Class manifest, final byte[] body, final int size) throws IOException { if (manifest == null) { throw new IllegalArgumentException("manifest can't be null"); } if (body == null) { throw new IllegalArgumentException("body can't be null"); } final JsonReader json = localReader.get().process(body, size); try { json.getNextToken(); final JsonReader.ReadObject simpleReader = tryFindReader(manifest); if (simpleReader != null) { return (TResult) simpleReader.read(json); } if (manifest.isArray()) { if (json.wasNull()) { return null; } else if (json.last() != '[') { throw json.newParseError("Expecting '[' for array start"); } final Class elementManifest = manifest.getComponentType(); final List list = deserializeList(elementManifest, body, size); if (list == null) { return null; } return (TResult) convertResultToArray(elementManifest, list); } if (fallback != null) { return (TResult) fallback.deserialize(context, manifest, body, size); } throw createErrorMessage(manifest); } finally { json.reset(); } } /** * Deserialize API for working with bytes. * Deserialize provided byte input into target object. *

* Since JSON is often though of as a series of char, * most libraries will convert inputs into a sequence of chars and do processing on them. * DslJson will treat input as a sequence of bytes which allows for various optimizations. * * @param manifest target type * @param body input JSON * @param size length * @return deserialized instance * @throws IOException error during deserialization */ @Nullable public Object deserialize( final Type manifest, final byte[] body, final int size) throws IOException { if (manifest instanceof Class) { return deserialize((Class) manifest, body, size); } if (manifest == null) { throw new IllegalArgumentException("manifest can't be null"); } if (body == null) { throw new IllegalArgumentException("body can't be null"); } final JsonReader json = localReader.get().process(body, size); try { json.getNextToken(); final Object result = deserializeWith(manifest, json); if (result != unknownValue) return result; if (fallback != null) { return fallback.deserialize(context, manifest, body, size); } throw new ConfigurationException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); } finally { json.reset(); } } @SuppressWarnings("unchecked") @Nullable private Object deserializeWith(Type manifest, JsonReader json) throws IOException { final JsonReader.ReadObject simpleReader = tryFindReader(manifest); if (simpleReader != null) { return simpleReader.read(json); } if (manifest instanceof ParameterizedType) { final ParameterizedType pt = (ParameterizedType) manifest; if (pt.getActualTypeArguments().length == 1) { final Type content = pt.getActualTypeArguments()[0]; final Class container = (Class) pt.getRawType(); if (container.isArray() || Collection.class.isAssignableFrom(container)) { if (json.wasNull()) { return null; } else if (json.last() != '[') { throw json.newParseError("Expecting '[' for array start"); } if (json.getNextToken() == ']') { if (container.isArray()) { returnEmptyArray(content); } return new ArrayList(0); } final JsonReader.ReadObject contentReader = tryFindReader(content); if (contentReader != null) { final ArrayList result = json.deserializeNullableCollection(contentReader); if (container.isArray()) { return returnAsArray(content, result); } return result; } } } } else if (manifest instanceof GenericArrayType) { if (json.wasNull()) { return null; } else if (json.last() != '[') { throw json.newParseError("Expecting '[' for array start"); } final Type content = ((GenericArrayType) manifest).getGenericComponentType(); if (json.getNextToken() == ']') { return returnEmptyArray(content); } final JsonReader.ReadObject contentReader = tryFindReader(content); if (contentReader != null) { final ArrayList result = json.deserializeNullableCollection(contentReader); return returnAsArray(content, result); } } return unknownValue; } private static Object returnAsArray(final Type content, final ArrayList result) { if (content instanceof Class) { return convertResultToArray((Class) content, result); } if (content instanceof ParameterizedType) { final ParameterizedType cpt = (ParameterizedType) content; return result.toArray((Object[]) Array.newInstance((Class) cpt.getRawType(), 0)); } return result.toArray(); } private static Object returnEmptyArray(Type content) { if (content instanceof Class) { return Array.newInstance((Class) content, 0); } if (content instanceof ParameterizedType) { final ParameterizedType pt = (ParameterizedType) content; return Array.newInstance((Class) pt.getRawType(), 0); } return new Object[0]; } private IOException createErrorMessage(final Class manifest) { final ArrayList> signatures = new ArrayList>(); findAllSignatures(manifest, signatures); for (final Class sig : signatures) { if (readers.containsKey(sig)) { if (sig.equals(manifest)) { return new IOException("Reader for provided type: " + manifest + " is disabled and fallback serialization is not registered (converter is registered as null).\n" + "Try initializing system with custom fallback or don't register null for " + manifest); } return new IOException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + "Found reader for: " + sig + " so try deserializing into that instead?\n" + "Alternatively, try initializing system with custom fallback or register specified type using registerReader into " + getClass()); } } return new IOException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); } /** * Convenient deserialize list API for working with bytes. * Deserialize provided byte input into target object. *

* Since JSON is often though of as a series of char, * most libraries will convert inputs into a sequence of chars and do processing on them. * DslJson will treat input as a sequence of bytes which allows for various optimizations. * * @param manifest target type * @param body input JSON * @param size length * @param target element type * @return deserialized list instance * @throws IOException error during deserialization */ @SuppressWarnings("unchecked") @Nullable public List deserializeList( final Class manifest, final byte[] body, final int size) throws IOException { if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); if (body == null) throw new IllegalArgumentException("body can't be null"); if (size == 4 && body[0] == 'n' && body[1] == 'u' && body[2] == 'l' && body[3] == 'l') { return null; } else if (size == 2 && body[0] == '[' && body[1] == ']') { return new ArrayList(0); } final JsonReader json = localReader.get().process(body, size); try { if (json.getNextToken() != '[') { if (json.wasNull()) { return null; } throw json.newParseError("Expecting '[' for list start"); } if (json.getNextToken() == ']') { return new ArrayList(0); } //leave for now in to avoid overhead of going through redirection via generic tryFindReader if (JsonObject.class.isAssignableFrom(manifest)) { final JsonReader.ReadJsonObject reader = getObjectReader(manifest); if (reader != null) { return (List) json.deserializeNullableCollection(reader); } } final JsonReader.ReadObject simpleReader = tryFindReader(manifest); if (simpleReader != null) { return json.deserializeNullableCollection(simpleReader); } if (fallback != null) { final Object array = Array.newInstance(manifest, 0); final TResult[] result = (TResult[]) fallback.deserialize(context, array.getClass(), body, size); if (result == null) { return null; } final ArrayList list = new ArrayList(result.length); for (TResult aResult : result) { list.add(aResult); } return list; } throw createErrorMessage(manifest); } finally { json.reset(); } } /** * This is deprecated to avoid using it. * Use deserializeList method without the buffer argument instead. * * Convenient deserialize list API for working with streams. * Deserialize provided stream input into target object. * Use buffer for internal conversion from stream into byte[] for partial processing. * This method creates a new instance of JsonReader. * There is also deserializeList without the buffer which reuses thread local reader. *

* Since JSON is often though of as a series of char, * most libraries will convert inputs into a sequence of chars and do processing on them. * DslJson will treat input as a sequence of bytes which allows for various optimizations. *

* When working on InputStream DslJson will process JSON in chunks of byte[] inputs. * Provided buffer will be used as input for partial processing. *

* For best performance buffer should be reused. * * @param manifest target type * @param stream input JSON * @param buffer buffer used for InputStream -> byte[] conversion * @param target element type * @return deserialized list * @throws IOException error during deserialization */ @SuppressWarnings("unchecked") @Nullable public List deserializeList( final Class manifest, final InputStream stream, final byte[] buffer) throws IOException { if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); if (stream == null) throw new IllegalArgumentException("stream can't be null"); if (buffer == null) throw new IllegalArgumentException("buffer can't be null"); return deserializeList(manifest, newReader(stream, buffer), stream); } /** * Convenient deserialize list API for working with streams. * Deserialize provided stream input into target object. *

* Since JSON is often though of as a series of char, * most libraries will convert inputs into a sequence of chars and do processing on them. * DslJson will treat input as a sequence of bytes which allows for various optimizations. *

* When working on InputStream DslJson will process JSON in chunks of byte[] inputs. *

* * @param manifest target type * @param stream input JSON * @param target element type * @return deserialized list * @throws IOException error during deserialization */ @SuppressWarnings("unchecked") @Nullable public List deserializeList( final Class manifest, final InputStream stream) throws IOException { if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); if (stream == null) throw new IllegalArgumentException("stream can't be null"); final JsonReader json = localReader.get().process(stream); try { return deserializeList(manifest, json, stream); } finally { json.reset(); } } @SuppressWarnings("unchecked") @Nullable private List deserializeList( final Class manifest, JsonReader json, InputStream stream) throws IOException { if (json.getNextToken() != '[') { if (json.wasNull()) { return null; } throw json.newParseError("Expecting '[' for list start"); } if (json.getNextToken() == ']') { return new ArrayList(0); } //leave for now in to avoid overhead of going through redirection via generic tryFindReader if (JsonObject.class.isAssignableFrom(manifest)) { final JsonReader.ReadJsonObject reader = getObjectReader(manifest); if (reader != null) { return (List) json.deserializeNullableCollection(reader); } } final JsonReader.ReadObject simpleReader = tryFindReader(manifest); if (simpleReader != null) { return json.deserializeNullableCollection(simpleReader); } if (fallback != null) { final Object array = Array.newInstance(manifest, 0); final TResult[] result = (TResult[]) fallback.deserialize(context, array.getClass(), new RereadStream(json.buffer, stream)); if (result == null) { return null; } final ArrayList list = new ArrayList(result.length); for (TResult aResult : result) { list.add(aResult); } return list; } throw createErrorMessage(manifest); } /** * Convenient deserialize API for working with streams. * Deserialize provided stream input into target object. * This method accepts a buffer and will create a new reader using provided buffer. * This buffer is used for internal conversion from stream into byte[] for partial processing. * There is also method without the buffer which reuses local thread reader for processing. *

* Since JSON is often though of as a series of char, * most libraries will convert inputs into a sequence of chars and do processing on them. * DslJson will treat input as a sequence of bytes which allows for various optimizations. *

* When working on InputStream DslJson will process JSON in chunks of byte[] inputs. * Provided buffer will be used as input for partial processing. *

* For best performance buffer should be reused. * * @param manifest target type * @param stream input JSON * @param buffer buffer used for InputStream -> byte[] conversion * @param target type * @return deserialized instance * @throws IOException error during deserialization */ @SuppressWarnings("unchecked") @Nullable public TResult deserialize( final Class manifest, final InputStream stream, final byte[] buffer) throws IOException { if (manifest == null) { throw new IllegalArgumentException("manifest can't be null"); } if (stream == null) { throw new IllegalArgumentException("stream can't be null"); } if (buffer == null) { throw new IllegalArgumentException("buffer can't be null"); } return deserialize(manifest, newReader(stream, buffer), stream); } /** * Convenient deserialize API for working with streams. * Deserialize provided stream input into target object. * This method reuses thread local reader for processing input stream. *

* Since JSON is often though of as a series of char, * most libraries will convert inputs into a sequence of chars and do processing on them. * DslJson will treat input as a sequence of bytes which allows for various optimizations. *

* When working on InputStream DslJson will process JSON in chunks of byte[] inputs. *

* * @param manifest target type * @param stream input JSON * @param target type * @return deserialized instance * @throws IOException error during deserialization */ @SuppressWarnings("unchecked") @Nullable public TResult deserialize( final Class manifest, final InputStream stream) throws IOException { if (manifest == null) { throw new IllegalArgumentException("manifest can't be null"); } if (stream == null) { throw new IllegalArgumentException("stream can't be null"); } final JsonReader json = localReader.get().process(stream); try { return deserialize(manifest, json, stream); } finally { json.reset(); } } @SuppressWarnings("unchecked") @Nullable private TResult deserialize( final Class manifest, final JsonReader json, final InputStream stream) throws IOException { json.getNextToken(); final JsonReader.ReadObject simpleReader = tryFindReader(manifest); if (simpleReader != null) { return (TResult) simpleReader.read(json); } if (manifest.isArray()) { if (json.wasNull()) { return null; } else if (json.last() != '[') { throw json.newParseError("Expecting '[' for array start"); } final Class elementManifest = manifest.getComponentType(); if (json.getNextToken() == ']') { return (TResult) Array.newInstance(elementManifest, 0); } //leave for now in to avoid overhead of going through redirection via generic tryFindReader if (JsonObject.class.isAssignableFrom(elementManifest)) { final JsonReader.ReadJsonObject objectReader = getObjectReader(elementManifest); if (objectReader != null) { List list = json.deserializeNullableCollection(objectReader); return (TResult) convertResultToArray(elementManifest, list); } } final JsonReader.ReadObject simpleElementReader = tryFindReader(elementManifest); if (simpleElementReader != null) { List list = json.deserializeNullableCollection(simpleElementReader); return (TResult) convertResultToArray(elementManifest, list); } } if (fallback != null) { return (TResult) fallback.deserialize(context, manifest, new RereadStream(json.buffer, stream)); } throw createErrorMessage(manifest); } /** * Deserialize API for working with streams. * Deserialize provided stream input into target object. * Use buffer for internal conversion from stream into byte[] for partial processing. * This method creates a new instance of JsonReader for processing the stream. * There is also a method without the byte[] buffer which reuses thread local reader. *

* Since JSON is often though of as a series of char, * most libraries will convert inputs into a sequence of chars and do processing on them. * DslJson will treat input as a sequence of bytes which allows for various optimizations. *

* When working on InputStream DslJson will process JSON in chunks of byte[] inputs. * Provided buffer will be used as input for partial processing. *

* For best performance buffer should be reused. * * @param manifest target type * @param stream input JSON * @param buffer buffer used for InputStream -> byte[] conversion * @return deserialized instance * @throws IOException error during deserialization */ @Nullable public Object deserialize( final Type manifest, final InputStream stream, final byte[] buffer) throws IOException { if (manifest instanceof Class) { return deserialize((Class) manifest, stream, buffer); } if (manifest == null) { throw new IllegalArgumentException("manifest can't be null"); } if (stream == null) { throw new IllegalArgumentException("stream can't be null"); } if (buffer == null) { throw new IllegalArgumentException("buffer can't be null"); } final JsonReader json = newReader(stream, buffer); json.getNextToken(); final Object result = deserializeWith(manifest, json); if (result != unknownValue) return result; if (fallback != null) { return fallback.deserialize(context, manifest, new RereadStream(buffer, stream)); } throw new ConfigurationException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); } /** * Deserialize API for working with streams. * Deserialize provided stream input into target object. * This method reuses thread local reader for processing JSON input. *

* Since JSON is often though of as a series of char, * most libraries will convert inputs into a sequence of chars and do processing on them. *

* When working on InputStream DslJson will process JSON in chunks of byte[] inputs. *

* * @param manifest target type * @param stream input JSON * @return deserialized instance * @throws IOException error during deserialization */ @Nullable public Object deserialize( final Type manifest, final InputStream stream) throws IOException { if (manifest instanceof Class) { return deserialize((Class) manifest, stream); } if (manifest == null) { throw new IllegalArgumentException("manifest can't be null"); } if (stream == null) { throw new IllegalArgumentException("stream can't be null"); } final JsonReader json = localReader.get().process(stream); try { json.getNextToken(); final Object result = deserializeWith(manifest, json); if (result != unknownValue) return result; if (fallback != null) { return fallback.deserialize(context, manifest, new RereadStream(json.buffer, stream)); } throw new ConfigurationException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); } finally { json.reset(); } } static class RereadStream extends InputStream { private final byte[] buffer; private final InputStream stream; private boolean usingBuffer; private int position; RereadStream(byte[] buffer, InputStream stream) { this.buffer = buffer; this.stream = stream; usingBuffer = true; } @Override public int read() throws IOException { if (usingBuffer) { if (position < buffer.length) { return buffer[position++]; } else usingBuffer = false; } return stream.read(); } @Override public int read(byte[] buf) throws IOException { if (usingBuffer) { return super.read(buf); } return stream.read(buf); } @Override public int read(byte[] buf, int off, int len) throws IOException { if (usingBuffer) { return super.read(buf, off, len); } return stream.read(buf, off, len); } } private static final Iterator EMPTY_ITERATOR = new Iterator() { @Override public boolean hasNext() { return false; } @Override public void remove() { } @Nullable @Override public Object next() { return null; } }; /** * Streaming API for collection deserialization. * DslJson will create iterator based on provided manifest info. * It will attempt to deserialize from stream on each next() invocation. * This method requires buffer instance for partial stream processing. * It will create a new instance of JsonReader. * There is also a method without the buffer which will reuse thread local reader. *

* Useful for processing very large streams if only one instance from collection is required at once. *

* Stream will be processed in chunks of specified buffer byte[]. * It will block on reading until buffer is full or end of stream is detected. * * @param manifest type info * @param stream JSON data stream * @param type info * @return Iterator to instances deserialized from input JSON * @throws IOException if reader is not found or there is an error processing input stream */ @SuppressWarnings("unchecked") @Nullable public Iterator iterateOver( final Class manifest, final InputStream stream) throws IOException { if (manifest == null) { throw new IllegalArgumentException("manifest can't be null"); } if (stream == null) { throw new IllegalArgumentException("stream can't be null"); } final JsonReader json = localReader.get(); json.process(stream); return iterateOver(manifest, json, stream); } /** * Streaming API for collection deserialization. * DslJson will create iterator based on provided manifest info. * It will attempt to deserialize from stream on each next() invocation. * This method requires buffer instance for partial stream processing. * It will create a new instance of JsonReader. * There is also a method without the buffer which will reuse thread local reader. *

* Useful for processing very large streams if only one instance from collection is required at once. *

* Stream will be processed in chunks of specified buffer byte[]. * It will block on reading until buffer is full or end of stream is detected. * * @param manifest type info * @param stream JSON data stream * @param buffer size of processing chunk * @param type info * @return Iterator to instances deserialized from input JSON * @throws IOException if reader is not found or there is an error processing input stream */ @SuppressWarnings("unchecked") @Nullable public Iterator iterateOver( final Class manifest, final InputStream stream, final byte[] buffer) throws IOException { if (manifest == null) { throw new IllegalArgumentException("manifest can't be null"); } if (stream == null) { throw new IllegalArgumentException("stream can't be null"); } if (buffer == null) { throw new IllegalArgumentException("buffer can't be null"); } return iterateOver(manifest, newReader(stream, buffer), stream); } @SuppressWarnings("unchecked") @Nullable private Iterator iterateOver( final Class manifest, final JsonReader json, final InputStream stream) throws IOException { if (json.getNextToken() != '[') { if (json.wasNull()) { return null; } throw json.newParseError("Expecting '[' for iterator start"); } if (json.getNextToken() == ']') { return EMPTY_ITERATOR; } //leave for now in to avoid overhead of going through redirection via generic tryFindReader if (JsonObject.class.isAssignableFrom(manifest)) { final JsonReader.ReadJsonObject reader = getObjectReader(manifest); if (reader != null) { return json.iterateOver(reader); } } final JsonReader.ReadObject simpleReader = tryFindReader(manifest); if (simpleReader != null) { return json.iterateOver(simpleReader); } if (fallback != null) { final Object array = Array.newInstance(manifest, 0); final TResult[] result = (TResult[]) fallback.deserialize(context, array.getClass(), new RereadStream(json.buffer, stream)); if (result == null) { return null; } final ArrayList list = new ArrayList(result.length); for (TResult aResult : result) { list.add(aResult); } return list.iterator(); } throw createErrorMessage(manifest); } private final JsonWriter.WriteObject OBJECT_WRITER = new JsonWriter.WriteObject() { @Override public void write(JsonWriter writer, @Nullable JsonObject value) { if (value == null) writer.writeNull(); else value.serialize(writer, omitDefaults); } }; private JsonReader.ReadObject convertToReader(final JsonReader.ReadJsonObject decoder) { return new JsonReader.ReadObject() { @Override public T read(JsonReader reader) throws IOException { if (reader.wasNull()) return null; else if (reader.last() != '{') throw reader.newParseError("Expecting '{' for object start"); reader.getNextToken(); return decoder.deserialize(reader); } }; } private final JsonWriter.WriteObject OBJECT_ARRAY_WRITER = new JsonWriter.WriteObject() { @Override public void write(JsonWriter writer, @Nullable Object value) { serialize(writer, (JsonObject[]) value); } }; private static final JsonWriter.WriteObject CHAR_ARRAY_WRITER = new JsonWriter.WriteObject() { @Override public void write(JsonWriter writer, @Nullable Object value) { StringConverter.serialize(new String((char[]) value), writer); } }; private final JsonWriter.WriteObject NULL_WRITER = new JsonWriter.WriteObject() { @Override public void write(JsonWriter writer, @Nullable Object value) { writer.writeNull(); } }; @SuppressWarnings("unchecked") private JsonWriter.WriteObject getOrCreateWriter(@Nullable final Object instance, final Class instanceManifest) throws IOException { if (instance instanceof JsonObject) { return OBJECT_WRITER; } if (instance instanceof JsonObject[]) { return OBJECT_ARRAY_WRITER; } final Class manifest = instanceManifest != null ? instanceManifest : instance.getClass(); if (instanceManifest != null) { if (JsonObject.class.isAssignableFrom(manifest)) { return OBJECT_WRITER; } } final JsonWriter.WriteObject simpleWriter = tryFindWriter(manifest); if (simpleWriter != null) { return simpleWriter; } if (manifest.isArray()) { final Class elementManifest = manifest.getComponentType(); if (char.class == elementManifest) { return CHAR_ARRAY_WRITER; } else { final JsonWriter.WriteObject elementWriter = tryFindWriter(elementManifest); if (elementWriter != null) { //TODO: cache writer for next lookup return new JsonWriter.WriteObject() { @Override public void write(JsonWriter writer, @Nullable Object value) { writer.serialize((Object[]) value, elementWriter); } }; } } } if (instance instanceof Collection || Collection.class.isAssignableFrom(manifest)) { return new JsonWriter.WriteObject() { @Override public void write(JsonWriter writer, @Nullable final Object value) { final Collection items = (Collection) value; Class baseType = null; final Iterator iterator = items.iterator(); //TODO: pick lowest common denominator!? do { final Object item = iterator.next(); if (item != null) { Class elementType = item.getClass(); if (elementType != baseType) { if (baseType == null || elementType.isAssignableFrom(baseType)) { baseType = elementType; } } } } while (iterator.hasNext()); if (baseType == null) { writer.writeByte(JsonWriter.ARRAY_START); writer.writeNull(); for (int i = 1; i < items.size(); i++) { writer.writeAscii(",null"); } writer.writeByte(JsonWriter.ARRAY_END); } else if (JsonObject.class.isAssignableFrom(baseType)) { serialize(writer, (Collection) items); } else { final JsonWriter.WriteObject elementWriter = tryFindWriter(baseType); if (elementWriter != null) { writer.serialize(items, elementWriter); } else if (fallback != null) { final ByteArrayOutputStream stream = new ByteArrayOutputStream(); stream.reset(); try { fallback.serialize(value, stream); } catch (IOException ex) { throw new SerializationException(ex); } writer.writeAscii(stream.toByteArray()); } else { throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + items.getClass()); } } } }; } throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + manifest); } /** * Streaming API for collection serialization. *

* It will iterate over entire iterator and serialize each instance into target output stream. * After each instance serialization it will copy JSON into target output stream. * During each serialization reader will be looked up based on next() instance which allows * serializing collection with different types. * If collection contains all instances of the same type, prefer the other streaming API. *

* If reader is not found an IOException will be thrown *

* If JsonWriter is provided it will be used, otherwise a new instance will be internally created. * * @param iterator input data * @param stream target JSON stream * @param writer temporary buffer for serializing a single item. Can be null * @param input data type * @throws IOException reader is not found, there is an error during serialization or problem with writing to target stream */ @SuppressWarnings("unchecked") public void iterateOver( final Iterator iterator, final OutputStream stream, @Nullable final JsonWriter writer) throws IOException { if (iterator == null) { throw new IllegalArgumentException("iterator can't be null"); } if (stream == null) { throw new IllegalArgumentException("stream can't be null"); } stream.write(JsonWriter.ARRAY_START); if (!iterator.hasNext()) { stream.write(JsonWriter.ARRAY_END); return; } final JsonWriter buffer = writer == null ? new JsonWriter(this) : writer; T item = iterator.next(); Class lastManifest = null; JsonWriter.WriteObject lastWriter = null; if (item != null) { lastManifest = item.getClass(); lastWriter = getOrCreateWriter(item, lastManifest); buffer.reset(); try { lastWriter.write(buffer, item); } catch (ConfigurationException e) { throw e; } catch (Exception e) { throw new IOException(e); } buffer.toStream(stream); } else { stream.write(NULL); } while (iterator.hasNext()) { stream.write(JsonWriter.COMMA); item = iterator.next(); if (item != null) { final Class currentManifest = item.getClass(); if (lastWriter == null || lastManifest == null || !lastManifest.equals(currentManifest)) { lastManifest = currentManifest; lastWriter = getOrCreateWriter(item, lastManifest); } buffer.reset(); try { lastWriter.write(buffer, item); } catch (ConfigurationException e) { throw e; } catch (Exception e) { throw new IOException(e); } buffer.toStream(stream); } else { stream.write(NULL); } } stream.write(JsonWriter.ARRAY_END); } /** * Streaming API for collection serialization. *

* It will iterate over entire iterator and serialize each instance into target output stream. * After each instance serialization it will copy JSON into target output stream. *

* If reader is not found an IOException will be thrown *

* If JsonWriter is provided it will be used, otherwise a new instance will be internally created. * * @param iterator input data * @param manifest type of elements in collection * @param stream target JSON stream * @param writer temporary buffer for serializing a single item. Can be null * @param input data type * @throws IOException reader is not found, there is an error during serialization or problem with writing to target stream */ @SuppressWarnings("unchecked") public void iterateOver( final Iterator iterator, final Class manifest, final OutputStream stream, @Nullable final JsonWriter writer) throws IOException { if (iterator == null) { throw new IllegalArgumentException("iterator can't be null"); } if (manifest == null) { throw new IllegalArgumentException("manifest can't be null"); } if (stream == null) { throw new IllegalArgumentException("stream can't be null"); } final JsonWriter buffer = writer == null ? new JsonWriter(this) : writer; final JsonWriter.WriteObject instanceWriter = getOrCreateWriter(null, manifest); stream.write(JsonWriter.ARRAY_START); T item = iterator.next(); if (item != null) { buffer.reset(); try { instanceWriter.write(buffer, item); } catch (ConfigurationException e) { throw e; } catch (Exception e) { throw new IOException(e); } buffer.toStream(stream); } else { stream.write(NULL); } while (iterator.hasNext()) { stream.write(JsonWriter.COMMA); item = iterator.next(); if (item != null) { buffer.reset(); try { instanceWriter.write(buffer, item); } catch (ConfigurationException e) { throw e; } catch (Exception e) { throw new IOException(e); } buffer.toStream(stream); } else { stream.write(NULL); } } stream.write(JsonWriter.ARRAY_END); } /** * Use writer.serialize instead * * @param writer writer * @param array items * @param type */ @Deprecated public void serialize(final JsonWriter writer, @Nullable final T[] array) { if (array == null) { writer.writeNull(); return; } writer.writeByte(JsonWriter.ARRAY_START); if (array.length != 0) { T item = array[0]; if (item != null) { item.serialize(writer, omitDefaults); } else { writer.writeNull(); } for (int i = 1; i < array.length; i++) { writer.writeByte(JsonWriter.COMMA); item = array[i]; if (item != null) { item.serialize(writer, omitDefaults); } else { writer.writeNull(); } } } writer.writeByte(JsonWriter.ARRAY_END); } /** * Use writer.serialize instead * * @param writer writer * @param array items * @param len part of array * @param type */ @Deprecated public void serialize(final JsonWriter writer, final T[] array, final int len) { if (writer == null) { throw new IllegalArgumentException("writer can't be null"); } if (array == null) { writer.writeNull(); return; } writer.writeByte(JsonWriter.ARRAY_START); if (len != 0) { T item = array[0]; if (item != null) { item.serialize(writer, omitDefaults); } else { writer.writeNull(); } for (int i = 1; i < len; i++) { writer.writeByte(JsonWriter.COMMA); item = array[i]; if (item != null) { item.serialize(writer, omitDefaults); } else { writer.writeNull(); } } } writer.writeByte(JsonWriter.ARRAY_END); } /** * Use writer.serialize instead * * @param writer writer * @param list items * @param type */ @Deprecated public void serialize(final JsonWriter writer, @Nullable final List list) { if (writer == null) { throw new IllegalArgumentException("writer can't be null"); } if (list == null) { writer.writeNull(); return; } writer.writeByte(JsonWriter.ARRAY_START); if (list.size() != 0) { T item = list.get(0); if (item != null) { item.serialize(writer, omitDefaults); } else { writer.writeNull(); } for (int i = 1; i < list.size(); i++) { writer.writeByte(JsonWriter.COMMA); item = list.get(i); if (item != null) { item.serialize(writer, omitDefaults); } else { writer.writeNull(); } } } writer.writeByte(JsonWriter.ARRAY_END); } /** * Use writer.serialize instead * * @param writer writer * @param collection items * @param type */ @Deprecated public void serialize(final JsonWriter writer, @Nullable final Collection collection) { if (writer == null) { throw new IllegalArgumentException("writer can't be null"); } if (collection == null) { writer.writeNull(); return; } writer.writeByte(JsonWriter.ARRAY_START); if (!collection.isEmpty()) { final Iterator it = collection.iterator(); T item = it.next(); if (item != null) { item.serialize(writer, omitDefaults); } else { writer.writeNull(); } while (it.hasNext()) { writer.writeByte(JsonWriter.COMMA); item = it.next(); if (item != null) { item.serialize(writer, omitDefaults); } else { writer.writeNull(); } } } writer.writeByte(JsonWriter.ARRAY_END); } /** * Generic serialize API. * Based on provided type manifest converter will be chosen. * If converter is not found method will return false. *

* Resulting JSON will be written into provided writer argument. * In case of successful serialization true will be returned. *

* For best performance writer argument should be reused. * * @param writer where to write resulting JSON * @param manifest type manifest * @param value instance to serialize * @return successful serialization */ @SuppressWarnings("unchecked") public boolean serialize(final JsonWriter writer, final Type manifest, @Nullable final Object value) { if (writer == null) { throw new IllegalArgumentException("writer can't be null"); } if (value == null) { writer.writeNull(); return true; } else if (value instanceof JsonObject) { ((JsonObject) value).serialize(writer, omitDefaults); return true; } else if (value instanceof JsonObject[]) { serialize(writer, (JsonObject[]) value); return true; } final JsonWriter.WriteObject simpleWriter = tryFindWriter(manifest); if (simpleWriter != null) { simpleWriter.write(writer, value); return true; } Class container = null; if (manifest instanceof Class) { container = (Class) manifest; } if (container != null && container.isArray()) { if (Array.getLength(value) == 0) { writer.writeAscii("[]"); return true; } final Class elementManifest = container.getComponentType(); if (char.class == elementManifest) { //TODO? char[] !? StringConverter.serialize(new String((char[]) value), writer); return true; } else { final JsonWriter.WriteObject elementWriter = (JsonWriter.WriteObject) tryFindWriter(elementManifest); if (elementWriter != null) { writer.serialize((Object[]) value, elementWriter); return true; } } } if (value instanceof Collection) { final Collection items = (Collection) value; if (items.isEmpty()) { writer.writeAscii("[]"); return true; } Class baseType = null; final Iterator iterator = items.iterator(); final boolean isList = items instanceof List; final List values = isList ? (List) items : new ArrayList(); final ArrayList itemWriters = new ArrayList(); Class lastElementType = null; JsonWriter.WriteObject lastWriter = null; boolean hasUnknownWriter = false; //TODO: pick lowest common denominator!? do { final Object item = iterator.next(); if (!isList) { values.add(item); } if (item != null) { final Class elementType = item.getClass(); if (elementType != baseType) { if (baseType == null || elementType.isAssignableFrom(baseType)) { baseType = elementType; } } if (lastElementType != elementType) { lastElementType = elementType; lastWriter = tryFindWriter(elementType); } itemWriters.add(lastWriter); hasUnknownWriter = hasUnknownWriter || lastWriter == null; } else { itemWriters.add(NULL_WRITER); } } while (iterator.hasNext()); if (baseType != null && JsonObject.class.isAssignableFrom(baseType)) { writer.writeByte(JsonWriter.ARRAY_START); final Iterator iter = values.iterator(); final JsonObject first = (JsonObject) iter.next(); if (first != null) first.serialize(writer, omitDefaults); else writer.writeNull(); while (iter.hasNext()) { writer.writeByte(JsonWriter.COMMA); final JsonObject next = (JsonObject) iter.next(); if (next != null) next.serialize(writer, omitDefaults); else writer.writeNull(); } writer.writeByte(JsonWriter.ARRAY_END); return true; } if (!hasUnknownWriter) { writer.writeByte(JsonWriter.ARRAY_START); final Iterator iter = values.iterator(); int cur = 1; itemWriters.get(0).write(writer, iter.next()); while (iter.hasNext()) { writer.writeByte(JsonWriter.COMMA); itemWriters.get(cur++).write(writer, iter.next()); } writer.writeByte(JsonWriter.ARRAY_END); return true; } final JsonWriter.WriteObject elementWriter = (JsonWriter.WriteObject) tryFindWriter(baseType); if (elementWriter != null) { writer.serialize(items, elementWriter); return true; } } return false; } private static final byte[] NULL = new byte[]{'n', 'u', 'l', 'l'}; /** * Convenient serialize API. * In most cases JSON is serialized into target `OutputStream`. * This method will reuse thread local instance of `JsonWriter` and serialize JSON into it. * * @param value instance to serialize * @param stream where to write resulting JSON * @throws IOException error when unable to serialize instance */ public final void serialize(@Nullable final Object value, final OutputStream stream) throws IOException { if (stream == null) { throw new IllegalArgumentException("stream can't be null"); } if (value == null) { stream.write(NULL); return; } final JsonWriter jw = localWriter.get(); jw.reset(stream); final Class manifest = value.getClass(); if (!serialize(jw, manifest, value)) { if (fallback == null) { throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + manifest); } fallback.serialize(value, stream); } else { jw.flush(); jw.reset(null); } } /** * Main serialization API. * Convert object instance into JSON. *

* JsonWriter contains a growable byte[] where JSON will be serialized. * After serialization JsonWriter can be copied into OutputStream or it's byte[] can be obtained *

* For best performance reuse `JsonWriter` or even better call `JsonWriter.WriteObject` directly * * @param writer where to write resulting JSON * @param value object instance to serialize * @throws IOException error when unable to serialize instance */ public final void serialize(final JsonWriter writer, @Nullable final Object value) throws IOException { if (writer == null) { throw new IllegalArgumentException("writer can't be null"); } if (value == null) { writer.writeNull(); return; } final Class manifest = value.getClass(); if (!serialize(writer, manifest, value)) { if (fallback == null) { throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + manifest); } ByteArrayOutputStream stream = new ByteArrayOutputStream(); fallback.serialize(value, stream); writer.writeAscii(stream.toByteArray()); } } }