/*
* Copyright 2013 Nicolas Morel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.nmorel.gwtjackson.client;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.fasterxml.jackson.annotation.ObjectIdGenerator;
import com.github.nmorel.gwtjackson.client.exception.JsonSerializationException;
import com.github.nmorel.gwtjackson.client.ser.bean.AbstractBeanJsonSerializer;
import com.github.nmorel.gwtjackson.client.ser.bean.ObjectIdSerializer;
import com.github.nmorel.gwtjackson.client.stream.JsonWriter;
import com.github.nmorel.gwtjackson.client.stream.impl.FastJsonWriter;
import com.google.gwt.core.client.GWT;
/**
* Context for the serialization process.
*
* @author Nicolas Morel
* @version $Id: $
*/
public class JsonSerializationContext extends JsonMappingContext {
/**
* Builder for {@link JsonSerializationContext}. To override default settings globally, you can extend this class, modify the
* default settings inside the constructor and tell the compiler to use your builder instead in your gwt.xml file :
* <pre>
* {@code
*
* <replace-with class="your.package.YourBuilder">
* <when-type-assignable class="com.github.nmorel.gwtjackson.client.JsonSerializationContext.Builder" />
* </replace-with>
*
* }
* </pre>
*/
public static class Builder {
protected boolean useEqualityForObjectId = false;
protected boolean serializeNulls = true;
protected boolean writeDatesAsTimestamps = true;
protected boolean writeDateKeysAsTimestamps = false;
protected boolean indent = false;
protected boolean wrapRootValue = false;
protected boolean writeCharArraysAsJsonArrays = false;
protected boolean writeNullMapValues = true;
protected boolean writeEmptyJsonArrays = true;
protected boolean orderMapEntriesByKeys = false;
protected boolean writeSingleElemArraysUnwrapped = false;
protected boolean wrapExceptions = true;
/**
* @deprecated Use {@link JsonSerializationContext#builder()} instead. This constructor will be made protected in v1.0.
*/
@Deprecated
public Builder() { }
/**
* Determines whether Object Identity is compared using
* true JVM-level identity of Object (false); or, <code>equals()</code> method.
* Latter is sometimes useful when dealing with Database-bound objects with
* ORM libraries (like Hibernate).
* <p>
* Option is disabled by default; meaning that strict identity is used, not
* <code>equals()</code>
* </p>
*
* @param useEqualityForObjectId true if should useEqualityForObjectId
*
* @return the builder
*/
public Builder useEqualityForObjectId( boolean useEqualityForObjectId ) {
this.useEqualityForObjectId = useEqualityForObjectId;
return this;
}
/**
* Sets whether object members are serialized when their value is null.
* This has no impact on array elements. The default is true.
*
* @param serializeNulls true if should serializeNulls
*
* @return the builder
*/
public Builder serializeNulls( boolean serializeNulls ) {
this.serializeNulls = serializeNulls;
return this;
}
/**
* Determines whether {@link java.util.Date} and {@link java.sql.Timestamp} values are to be serialized as numeric timestamps
* (true; the default), or as textual representation.
* <p>If textual representation is used, the actual format is
* {@link com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat#ISO_8601}</p>
* Option is enabled by default.
*
* @param writeDatesAsTimestamps true if should writeDatesAsTimestamps
*
* @return the builder
*/
public Builder writeDatesAsTimestamps( boolean writeDatesAsTimestamps ) {
this.writeDatesAsTimestamps = writeDatesAsTimestamps;
return this;
}
/**
* Feature that determines whether {@link java.util.Date}s and {@link java.sql.Timestamp}s used as {@link java.util.Map} keys are
* serialized as timestamps or as textual values.
* <p>If textual representation is used, the actual format is
* {@link com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat#ISO_8601}</p>
* Option is disabled by default.
*
* @param writeDateKeysAsTimestamps true if should writeDateKeysAsTimestamps
*
* @return the builder
*/
public Builder writeDateKeysAsTimestamps( boolean writeDateKeysAsTimestamps ) {
this.writeDateKeysAsTimestamps = writeDateKeysAsTimestamps;
return this;
}
/**
* Feature that allows enabling (or disabling) indentation
* for the underlying writer.
* <p>Feature is disabled by default.</p>
*
* @param indent true if should indent
*
* @return the builder
*/
public Builder indent( boolean indent ) {
this.indent = indent;
return this;
}
/**
* Feature that can be enabled to make root value (usually JSON
* Object but can be any type) wrapped within a single property
* JSON object, where key as the "root name", as determined by
* annotation introspector or fallback (non-qualified
* class name).
* <p>Feature is disabled by default.</p>
*
* @param wrapRootValue true if should wrapRootValue
*
* @return the builder
*/
public Builder wrapRootValue( boolean wrapRootValue ) {
this.wrapRootValue = wrapRootValue;
return this;
}
/**
* Feature that determines how type <code>char[]</code> is serialized:
* when enabled, will be serialized as an explict JSON array (with
* single-character Strings as values); when disabled, defaults to
* serializing them as Strings (which is more compact).
* <p>
* Feature is disabled by default.
* </p>
*
* @param writeCharArraysAsJsonArrays true if should writeCharArraysAsJsonArrays
*
* @return the builder
*/
public Builder writeCharArraysAsJsonArrays( boolean writeCharArraysAsJsonArrays ) {
this.writeCharArraysAsJsonArrays = writeCharArraysAsJsonArrays;
return this;
}
/**
* Feature that determines whether Map entries with null values are
* to be serialized (true) or not (false).
* <p>
* Feature is enabled by default.
* </p>
*
* @param writeNullMapValues true if should writeNullMapValues
*
* @return the builder
*/
public Builder writeNullMapValues( boolean writeNullMapValues ) {
this.writeNullMapValues = writeNullMapValues;
return this;
}
/**
* Feature that determines whether Container properties (POJO properties
* with declared value of Collection or array; i.e. things that produce JSON
* arrays) that are empty (have no elements)
* will be serialized as empty JSON arrays (true), or suppressed from output (false).
* <p>
* Note that this does not change behavior of {@link java.util.Map}s, or
* "Collection-like" types.
* </p>
* <p>
* Feature is enabled by default.
* </p>
*
* @param writeEmptyJsonArrays true if should writeEmptyJsonArrays
*
* @return the builder
*/
public Builder writeEmptyJsonArrays( boolean writeEmptyJsonArrays ) {
this.writeEmptyJsonArrays = writeEmptyJsonArrays;
return this;
}
/**
* Feature that determines whether {@link java.util.Map} entries are first
* sorted by key before serialization or not: if enabled, additional sorting
* step is performed if necessary (not necessary for {@link java.util.SortedMap}s),
* if disabled, no additional sorting is needed.
* <p>
* Feature is disabled by default.
* </p>
*
* @param orderMapEntriesByKeys true if should orderMapEntriesByKeys
*
* @return the builder
*/
public Builder orderMapEntriesByKeys( boolean orderMapEntriesByKeys ) {
this.orderMapEntriesByKeys = orderMapEntriesByKeys;
return this;
}
/**
* Feature added for interoperability, to work with oddities of
* so-called "BadgerFish" convention.
* Feature determines handling of single element {@link java.util.Collection}s
* and arrays: if enabled, {@link java.util.Collection}s and arrays that contain exactly
* one element will be serialized as if that element itself was serialized.
* <br>
* <br>
* When enabled, a POJO with array that normally looks like this:
* <pre>
* { "arrayProperty" : [ 1 ] }
* </pre>
* will instead be serialized as
* <pre>
* { "arrayProperty" : 1 }
* </pre>
* <p>
* Note that this feature is counterpart to {@link JsonDeserializationContext.Builder#acceptSingleValueAsArray(boolean)}
* (that is, usually both are enabled, or neither is).
* </p>
* Feature is disabled by default, so that no special handling is done.
*
* @param writeSingleElemArraysUnwrapped true if should writeSingleElemArraysUnwrapped
*
* @return the builder
*/
public Builder writeSingleElemArraysUnwrapped( boolean writeSingleElemArraysUnwrapped ) {
this.writeSingleElemArraysUnwrapped = writeSingleElemArraysUnwrapped;
return this;
}
/**
* Feature that determines whether gwt-jackson code should catch
* and wrap {@link RuntimeException}s (but never {@link Error}s!)
* to add additional information about
* location (within input) of problem or not. If enabled,
* exceptions will be caught and re-thrown; this can be
* convenient both in that all exceptions will be checked and
* declared, and so there is more contextual information.
* However, sometimes calling application may just want "raw"
* unchecked exceptions passed as is.
* <br>
* <br>
* Feature is enabled by default.
*
* @param wrapExceptions true if should wrapExceptions
*
* @return the builder
*/
public Builder wrapExceptions( boolean wrapExceptions ) {
this.wrapExceptions = wrapExceptions;
return this;
}
public final JsonSerializationContext build() {
return new JsonSerializationContext( useEqualityForObjectId, serializeNulls, writeDatesAsTimestamps,
writeDateKeysAsTimestamps, indent, wrapRootValue, writeCharArraysAsJsonArrays, writeNullMapValues,
writeEmptyJsonArrays, orderMapEntriesByKeys, writeSingleElemArraysUnwrapped, wrapExceptions );
}
}
public static class DefaultBuilder extends Builder {
private DefaultBuilder() { }
}
/**
* <p>builder</p>
*
* @return a {@link com.github.nmorel.gwtjackson.client.JsonSerializationContext.Builder} object.
*/
public static Builder builder() {
return GWT.create( Builder.class );
}
private static final Logger logger = Logger.getLogger( "JsonSerialization" );
private Map<Object, ObjectIdSerializer<?>> mapObjectId;
private List<ObjectIdGenerator<?>> generators;
/*
* Serialization options
*/
private final boolean useEqualityForObjectId;
private final boolean serializeNulls;
private final boolean writeDatesAsTimestamps;
private final boolean writeDateKeysAsTimestamps;
private final boolean indent;
private final boolean wrapRootValue;
private final boolean writeCharArraysAsJsonArrays;
private final boolean writeNullMapValues;
private final boolean writeEmptyJsonArrays;
private final boolean orderMapEntriesByKeys;
private final boolean writeSingleElemArraysUnwrapped;
private final boolean wrapExceptions;
private JsonSerializationContext( boolean useEqualityForObjectId, boolean serializeNulls, boolean writeDatesAsTimestamps, boolean
writeDateKeysAsTimestamps, boolean indent, boolean wrapRootValue, boolean writeCharArraysAsJsonArrays, boolean
writeNullMapValues, boolean writeEmptyJsonArrays, boolean orderMapEntriesByKeys, boolean
writeSingleElemArraysUnwrapped,
boolean wrapExceptions ) {
this.useEqualityForObjectId = useEqualityForObjectId;
this.serializeNulls = serializeNulls;
this.writeDatesAsTimestamps = writeDatesAsTimestamps;
this.writeDateKeysAsTimestamps = writeDateKeysAsTimestamps;
this.indent = indent;
this.wrapRootValue = wrapRootValue;
this.writeCharArraysAsJsonArrays = writeCharArraysAsJsonArrays;
this.writeNullMapValues = writeNullMapValues;
this.writeEmptyJsonArrays = writeEmptyJsonArrays;
this.orderMapEntriesByKeys = orderMapEntriesByKeys;
this.writeSingleElemArraysUnwrapped = writeSingleElemArraysUnwrapped;
this.wrapExceptions = wrapExceptions;
}
/**
* {@inheritDoc}
*/
@Override
public Logger getLogger() {
return logger;
}
/**
* <p>isSerializeNulls</p>
*
* @return a boolean.
* @see Builder#serializeNulls(boolean)
*/
public boolean isSerializeNulls() {
return serializeNulls;
}
/**
* <p>isWriteDatesAsTimestamps</p>
*
* @return a boolean.
* @see Builder#writeDatesAsTimestamps(boolean)
*/
public boolean isWriteDatesAsTimestamps() {
return writeDatesAsTimestamps;
}
/**
* <p>isWriteDateKeysAsTimestamps</p>
*
* @return a boolean.
* @see Builder#writeDateKeysAsTimestamps(boolean)
*/
public boolean isWriteDateKeysAsTimestamps() {
return writeDateKeysAsTimestamps;
}
/**
* <p>isWrapRootValue</p>
*
* @return a boolean.
* @see Builder#wrapRootValue(boolean)
*/
public boolean isWrapRootValue() {
return wrapRootValue;
}
/**
* <p>isWriteCharArraysAsJsonArrays</p>
*
* @return a boolean.
* @see Builder#writeCharArraysAsJsonArrays(boolean)
*/
public boolean isWriteCharArraysAsJsonArrays() {
return writeCharArraysAsJsonArrays;
}
/**
* <p>isWriteNullMapValues</p>
*
* @return a boolean.
* @see Builder#writeNullMapValues(boolean)
*/
public boolean isWriteNullMapValues() {
return writeNullMapValues;
}
/**
* <p>isWriteEmptyJsonArrays</p>
*
* @return a boolean.
* @see Builder#writeEmptyJsonArrays(boolean)
*/
public boolean isWriteEmptyJsonArrays() {
return writeEmptyJsonArrays;
}
/**
* <p>isOrderMapEntriesByKeys</p>
*
* @return a boolean.
* @see Builder#orderMapEntriesByKeys(boolean)
*/
public boolean isOrderMapEntriesByKeys() {
return orderMapEntriesByKeys;
}
/**
* <p>isWriteSingleElemArraysUnwrapped</p>
*
* @return a boolean.
* @see Builder#writeSingleElemArraysUnwrapped(boolean)
*/
public boolean isWriteSingleElemArraysUnwrapped() {
return writeSingleElemArraysUnwrapped;
}
/**
* <p>newJsonWriter</p>
*
* @return a {@link com.github.nmorel.gwtjackson.client.stream.JsonWriter} object.
*/
public JsonWriter newJsonWriter() {
JsonWriter writer = new FastJsonWriter( new StringBuilder() );
writer.setLenient( true );
if ( indent ) {
writer.setIndent( " " );
}
return writer;
}
/**
* Trace an error and returns a corresponding exception.
*
* @param value current value
* @param message error message
*
* @return a {@link JsonSerializationException} with the given message
*/
public JsonSerializationException traceError( Object value, String message ) {
getLogger().log( Level.SEVERE, message );
return new JsonSerializationException( message );
}
/**
* Trace an error with current writer state and returns a corresponding exception.
*
* @param value current value
* @param message error message
* @param writer current writer
*
* @return a {@link JsonSerializationException} with the given message
*/
public JsonSerializationException traceError( Object value, String message, JsonWriter writer ) {
JsonSerializationException exception = traceError( value, message );
traceWriterInfo( value, writer );
return exception;
}
/**
* Trace an error and returns a corresponding exception.
*
* @param value current value
* @param cause cause of the error
*
* @return a {@link JsonSerializationException} if we wrap the exceptions, the cause otherwise
*/
public RuntimeException traceError( Object value, RuntimeException cause ) {
getLogger().log( Level.SEVERE, "Error during serialization", cause );
if ( wrapExceptions ) {
return new JsonSerializationException( cause );
} else {
return cause;
}
}
/**
* Trace an error with current writer state and returns a corresponding exception.
*
* @param value current value
* @param cause cause of the error
* @param writer current writer
*
* @return a {@link JsonSerializationException} if we wrap the exceptions, the cause otherwise
*/
public RuntimeException traceError( Object value, RuntimeException cause, JsonWriter writer ) {
RuntimeException exception = traceError( value, cause );
traceWriterInfo( value, writer );
return exception;
}
/**
* Trace the current writer state
*
* @param value current value
*/
private void traceWriterInfo( Object value, JsonWriter writer ) {
if ( getLogger().isLoggable( Level.INFO ) ) {
getLogger().log( Level.INFO, "Error on value <" + value + ">. Current output : <" + writer.getOutput() + ">" );
}
}
/**
* <p>addObjectId</p>
*
* @param object a {@link java.lang.Object} object.
* @param id a {@link com.github.nmorel.gwtjackson.client.ser.bean.ObjectIdSerializer} object.
*/
public void addObjectId( Object object, ObjectIdSerializer<?> id ) {
if ( null == mapObjectId ) {
if ( useEqualityForObjectId ) {
mapObjectId = new HashMap<Object, ObjectIdSerializer<?>>();
} else {
mapObjectId = new IdentityHashMap<Object, ObjectIdSerializer<?>>();
}
}
mapObjectId.put( object, id );
}
/**
* <p>getObjectId</p>
*
* @param object a {@link java.lang.Object} object.
*
* @return a {@link com.github.nmorel.gwtjackson.client.ser.bean.ObjectIdSerializer} object.
*/
public ObjectIdSerializer<?> getObjectId( Object object ) {
if ( null != mapObjectId ) {
return mapObjectId.get( object );
}
return null;
}
/**
* Used by generated {@link AbstractBeanJsonSerializer}
*
* @param generator instance of generator to add
*/
@SuppressWarnings( "UnusedDeclaration" )
public void addGenerator( ObjectIdGenerator<?> generator ) {
if ( null == generators ) {
generators = new ArrayList<ObjectIdGenerator<?>>();
}
generators.add( generator );
}
/**
* Used by generated {@link AbstractBeanJsonSerializer}
*
* @param gen generator used to find equivalent generator
* @param <T> a T object.
*
* @return a {@link com.fasterxml.jackson.annotation.ObjectIdGenerator} object.
*/
@SuppressWarnings( {"UnusedDeclaration", "unchecked"} )
public <T> ObjectIdGenerator<T> findObjectIdGenerator( ObjectIdGenerator<T> gen ) {
if ( null != generators ) {
for ( ObjectIdGenerator<?> generator : generators ) {
if ( generator.canUseFor( gen ) ) {
return (ObjectIdGenerator<T>) generator;
}
}
}
return null;
}
}