/*************************************************************************
* Copyright 2009-2014 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.simpleworkflow;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Date;
import java.util.Locale;
import com.eucalyptus.simpleworkflow.common.model.SimpleWorkflowMessage;
import com.eucalyptus.util.Exceptions;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.std.DateDeserializers;
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
/**
*
*/
public class SwfJsonUtils {
private static final ObjectMapper mapper = new ObjectMapper( );
static {
final SimpleModule module = new SimpleModule( "SwfModule", new Version(1, 0, 0, null, null, null) )
.addSerializer( Date.class, new EpochSecondsDateSerializer( ) )
.addDeserializer( Date.class, new EpochSecondsDateDeserializer( ) );
mapper.registerModule( module );
mapper.setDateFormat( new EpochSecondsDateFormat() );
mapper.addMixInAnnotations( SimpleWorkflowMessage.class, BindingMixIn.class );
mapper.disable( SerializationFeature.FAIL_ON_EMPTY_BEANS );
mapper.disable( MapperFeature.AUTO_DETECT_IS_GETTERS );
mapper.setVisibilityChecker(new VisibilityChecker.Std(VisibilityChecker.Std.class.getAnnotation(JsonAutoDetect.class)){
private static final long serialVersionUID = 1L;
@Override
public boolean isSetterVisible( final Method m ) {
return !( m.getParameterCount( ) == 1 && m.getParameterTypes( )[ 0 ].isEnum( ) ) && super.isSetterVisible( m );
}
});
mapper.setSerializationInclusion( JsonInclude.Include.NON_NULL );
}
public static String writeObjectAsString( final Object object ) {
final ByteArrayOutputStream out = new ByteArrayOutputStream( 512 );
try {
mapper.writeValue( out, object );
} catch ( IOException ioe ) {
throw Exceptions.toUndeclared( ioe );
}
return new String( out.toByteArray( ), Charsets.UTF_8 );
}
public static void writeObject( final OutputStream out, final Object object ) throws IOException {
mapper.writeValue( out, object );
}
public static <T> T readObject( final String in, final Class<T> type ) throws IOException {
final JsonParser parser = mapper.getFactory( ).createJsonParser( new StringReader( in ) {
@Override public String toString() { return "message"; } // overridden for better source in error message
} );
final T result = mapper.readValue( parser, type );
boolean trailingContent;
try {
trailingContent = parser.nextToken( ) != null;
} catch ( IOException e ) {
trailingContent = true;
}
if ( trailingContent ) {
throw new IOException( "Unexpected trailing content at " + parser.getCurrentLocation( ) );
}
return result;
}
// TODO:STEVE: add unit test to ensure we don't start using unexpected properties from BaseMessage
// ignore properties of BaseMessage
@JsonIgnoreProperties( { "correlationId", "effectiveUserId", "reply", "statusMessage", "userId",
"_disabledServices", "_notreadyServices", "_stoppedServices", "_epoch", "_services", "_return",
"callerContext" } )
private interface BindingMixIn {
}
private static final class EpochSecondsDateDeserializer extends JsonDeserializer<Date> {
@Override
public Date deserialize( final JsonParser jsonParser,
final DeserializationContext deserializationContext ) throws IOException {
final JsonToken token = jsonParser.getCurrentToken( );
switch ( token ) {
case VALUE_NUMBER_FLOAT:
return new Date( jsonParser.getDecimalValue( ).movePointRight( 3 ).longValue( ) );
case VALUE_NUMBER_INT:
return new Date( jsonParser.getLongValue( ) * 1000L );
default:
return new DateDeserializers.DateDeserializer( ).deserialize( jsonParser, deserializationContext );
}
}
}
private static final class EpochSecondsDateSerializer extends JsonSerializer<Date> {
@Override
public void serialize( final Date date,
final JsonGenerator jsonGenerator,
final SerializerProvider serializerProvider ) throws IOException {
jsonGenerator.writeRawValue( String.valueOf( date.getTime( ) / 1000 ) + "." + Strings.padStart( Long.toString( date.getTime( ) % 1000 ), 3, '0' ) );
}
}
private static final class EpochSecondsDateFormat extends DateFormat implements Cloneable {
private static final long serialVersionUID = 1L;
@Override
public StringBuffer format( final Date date, final StringBuffer toAppendTo, final FieldPosition fieldPosition ) {
StringBuffer out = toAppendTo == null ? new StringBuffer( ) : toAppendTo;
if ( date != null ) {
out.append( date.getTime( ) / 1000 );
out.append( '.' );
out.append( Strings.padStart( Long.toString( date.getTime( ) % 1000 ), 3, '0' ) );
}
return out;
}
@Override
public Date parse( final String source, final ParsePosition pos ) {
if ( source != null ) try {
Number number = DecimalFormat.getInstance( new Locale( "en" ) ).parse( source );
pos.setIndex( source.length( ) ) ;
return new Date( (long)(number.doubleValue() * 1000d) );
} catch ( ParseException ignore ) {
}
return null;
}
@SuppressWarnings( "CloneDoesntCallSuperClone" )
@Override
public Object clone( ) {
return this;
}
}
}