blob: a6434761d2eb8345e57d99b8d5cf8b4e33d3349d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.zest.spi.value;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.Period;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.apache.zest.api.ZestAPI;
import org.apache.zest.api.association.Association;
import org.apache.zest.api.association.AssociationStateHolder;
import org.apache.zest.api.association.ManyAssociation;
import org.apache.zest.api.association.NamedAssociation;
import org.apache.zest.api.composite.CompositeInstance;
import org.apache.zest.api.entity.EntityComposite;
import org.apache.zest.api.entity.EntityReference;
import org.apache.zest.api.property.Property;
import org.apache.zest.api.value.ValueComposite;
import org.apache.zest.api.value.ValueDescriptor;
import org.apache.zest.api.value.ValueSerializationException;
import org.apache.zest.api.value.ValueSerializer;
/**
* Adapter for pull-parsing capable ValueSerializers.
*
* <p>
* Among Plain values (see {@link ValueSerializer}) some are considered primitives to underlying serialization
* mechanisms and by so handed/come without conversion to/from implementations. Primitive values can be one of:
* </p>
* <ul>
* <li>String,</li>
* <li>Character or char,</li>
* <li>Boolean or boolean,</li>
* <li>Integer or int,</li>
* <li>Long or long,</li>
* <li>Short or short,</li>
* <li>Byte or byte,</li>
* <li>Float or float,</li>
* <li>Double or double.</li>
* </ul>
* <p>
* Some other Plain values are transformed before being handed to implementations:
* </p>
* <ul>
* <li>BigInteger and BigDecimal depends on ValueSerializer.{@link org.apache.zest.api.value.ValueSerializer.Options};</li>
* <li>Date as a ISO-8601 UTC String;</li>
* <li>DateTime (JodaTime) as a ISO-8601 String with timezone offset or Z for UTC;</li>
* <li>LocalDateTime (JodaTime) as a ISO-8601 String with no timezone offset;</li>
* <li>LocalDate (JodaTime) as a ISO-8601 String with no time info;</li>
* </ul>
*
* @param <OutputType> Implementor output type
*/
public abstract class ValueSerializerAdapter<OutputType>
implements ValueSerializer
{
interface ComplexSerializer<T, OutputType>
{
void serialize( Options options, T object, OutputType output )
throws Exception;
}
private static final String UTF_8 = "UTF-8";
private static <TO, FROM extends TO> BiFunction<Options, FROM, TO> identitySerializer()
{
return ( options, from ) -> from;
}
private final Map<Class<?>, BiFunction<Options, Object, Object>> serializers = new HashMap<>( 16 );
private final Map<Class<?>, ComplexSerializer<Object, OutputType>> complexSerializers = new HashMap<>( 2 );
/**
* Register a Plain Value type serialization Function.
*
* @param <T> Plain Value parametrized Type
* @param type Plain Value Type
* @param serializer Serialization Function
*/
@SuppressWarnings( "unchecked" )
protected final <T> void registerSerializer( Class<T> type, BiFunction<Options, T, Object> serializer )
{
serializers.put( type, (BiFunction<Options, Object, Object>) serializer );
}
/**
* Register a Complex Value type serialization Function.
*
* @param <T> Complex Value parametrized Type
* @param type Complex Value Type
* @param serializer Serialization Function
*/
@SuppressWarnings( "unchecked" )
protected final <T> void registerComplexSerializer( Class<T> type, ComplexSerializer<T, OutputType> serializer )
{
complexSerializers.put( type, (ComplexSerializer<Object, OutputType>) serializer );
}
public ValueSerializerAdapter()
{
// Primitive Value types
registerSerializer( String.class, ValueSerializerAdapter.identitySerializer() );
registerSerializer( Character.class, ValueSerializerAdapter.identitySerializer() );
registerSerializer( Boolean.class, ValueSerializerAdapter.identitySerializer() );
registerSerializer( Integer.class, ValueSerializerAdapter.identitySerializer() );
registerSerializer( Long.class, ValueSerializerAdapter.identitySerializer() );
registerSerializer( Short.class, ValueSerializerAdapter.identitySerializer() );
registerSerializer( Byte.class, ValueSerializerAdapter.identitySerializer() );
registerSerializer( Float.class, ValueSerializerAdapter.identitySerializer() );
registerSerializer( Double.class, ValueSerializerAdapter.identitySerializer() );
// Number types
registerSerializer( BigDecimal.class, ( options, bigDecimal ) -> bigDecimal.toString() );
registerSerializer( BigInteger.class, ( options, bigInteger ) -> bigInteger.toString() );
// Date types
registerSerializer( Instant.class, ( options, date ) -> date.toString() );
registerSerializer( Duration.class, ( options, date ) -> date.toString() );
registerSerializer( Period.class, ( options, date ) -> date.toString() );
registerSerializer( ZonedDateTime.class, ( options, date ) -> date.toString() );
registerSerializer( OffsetDateTime.class, ( options, date ) -> date.toString() );
registerSerializer( LocalDateTime.class, ( options, date ) -> date.toString() );
registerSerializer( LocalDate.class, ( options, date ) -> date.toString() );
registerSerializer( LocalTime.class, ( options, date ) -> date.toString() );
// Other supported types
registerSerializer( EntityReference.class, ( options, ref ) -> ref.toString() );
}
@Override
public final <T> Function<T, String> serialize()
{
return this::serialize;
}
@Override
public final <T> Function<T, String> serialize( final Options options )
{
return object -> serialize( options, object );
}
@Override
public final String serialize( Object object )
throws ValueSerializationException
{
return serialize( new Options(true), object );
}
@Override
public final String serialize( Options options, Object object )
throws ValueSerializationException
{
try
{
ByteArrayOutputStream output = new ByteArrayOutputStream();
serializeRoot( options, object, output );
return output.toString( UTF_8 );
}
catch( ValueSerializationException ex )
{
throw ex;
}
catch( Exception ex )
{
throw new ValueSerializationException( "Could not serialize value", ex );
}
}
@Override
public final void serialize( Object object, OutputStream output )
throws ValueSerializationException
{
serialize( new Options(true), object, output );
}
@Override
public final void serialize( Options options, Object object, OutputStream output )
throws ValueSerializationException
{
try
{
serializeRoot( options, object, output );
}
catch( ValueSerializationException ex )
{
throw ex;
}
catch( Exception ex )
{
throw new ValueSerializationException( "Could not serialize value", ex );
}
}
private void serializeRoot( Options options, Object object, OutputStream output )
throws Exception
{
if( object != null )
{
if( serializers.get( object.getClass() ) != null )
{
// Plain Value
Object serialized = serializers.get( object.getClass() ).apply( options, object );
output.write( serialized.toString().getBytes( UTF_8 ) );
}
else if( object.getClass().isEnum() )
{
// Enum Value
output.write( object.toString().getBytes( UTF_8 ) );
}
else if( object.getClass().isArray() )
{
// Array Value
output.write( serializeBase64Serializable( object ).getBytes( UTF_8 ) );
}
else
{
// Complex Value
OutputType adaptedOutput = adaptOutput( output );
onSerializationStart( object, adaptedOutput );
doSerialize( options, object, adaptedOutput, true );
onSerializationEnd( object, adaptedOutput );
}
}
}
private void doSerialize( Options options, Object object, OutputType output, boolean rootPass )
throws Exception
{
// Null
if( object == null )
{
onValue( output, null );
}
else // Registered serializer
if( serializers.get( object.getClass() ) != null )
{
onValue( output, serializers.get( object.getClass() ).apply( options, object ) );
}
else if( complexSerializers.get( object.getClass() ) != null )
{
complexSerializers.get( object.getClass() ).serialize( options, object, output );
}
else // ValueComposite
if( ValueComposite.class.isAssignableFrom( object.getClass() ) )
{
serializeValueComposite( options, object, output, rootPass );
}
else // EntityComposite
if( EntityComposite.class.isAssignableFrom( object.getClass() ) )
{
serializeEntityComposite( object, output );
}
else // Collection - Iterable
if( Iterable.class.isAssignableFrom( object.getClass() ) )
{
serializeIterable( options, object, output );
}
else // Array - QUID Remove this and use java serialization for arrays?
if( object.getClass().isArray() )
{
serializeBase64Serializable( object, output );
}
else // Map
if( Map.class.isAssignableFrom( object.getClass() ) )
{
serializeMap( options, object, output );
}
else // Enum
if( object.getClass().isEnum() )
{
onValue( output, object.toString() );
}
else // Fallback to Base64 encoded Java Serialization
{
serializeBase64Serializable( object, output );
}
}
private void serializeValueComposite( Options options, Object object, OutputType output, boolean rootPass )
throws Exception
{
CompositeInstance valueInstance = ZestAPI.FUNCTION_COMPOSITE_INSTANCE_OF.apply( (ValueComposite) object );
ValueDescriptor descriptor = (ValueDescriptor) valueInstance.descriptor();
AssociationStateHolder state = (AssociationStateHolder) valueInstance.state();
onObjectStart( output );
//noinspection ConstantConditions
if( options.includeTypeInfo && !rootPass )
{
onFieldStart( output, "_type" );
onValueStart( output );
onValue( output, descriptor.valueType().types().findFirst().get().getName() );
onValueEnd( output );
onFieldEnd( output );
}
descriptor.valueType().properties().forEach( persistentProperty -> {
Property<?> property = state.propertyFor( persistentProperty.accessor() );
try
{
onFieldStart( output, persistentProperty.qualifiedName().name() );
onValueStart( output );
doSerialize( options, property.get(), output, false );
onValueEnd( output );
onFieldEnd( output );
}
catch( Exception e )
{
throw new ValueSerializationException( "Unable to serialize property " + persistentProperty, e );
}
} );
descriptor.valueType().associations().forEach( associationDescriptor -> {
Association<?> association = state.associationFor( associationDescriptor.accessor() );
try
{
onFieldStart( output, associationDescriptor.qualifiedName().name() );
onValueStart( output );
EntityReference ref = association.reference();
if( ref == null )
{
onValue( output, null );
}
else
{
onValue( output, ref.identity() );
}
onValueEnd( output );
onFieldEnd( output );
}
catch( Exception e )
{
throw new ValueSerializationException( "Unable to serialize association " + associationDescriptor, e );
}
} );
descriptor.valueType().manyAssociations().forEach( associationDescriptor -> {
ManyAssociation<?> manyAssociation = state.manyAssociationFor( associationDescriptor.accessor() );
try
{
onFieldStart( output, associationDescriptor.qualifiedName().name() );
onValueStart( output );
onArrayStart( output );
for( EntityReference ref : manyAssociation.references() )
{
onValueStart( output );
onValue( output, ref.identity() );
onValueEnd( output );
}
onArrayEnd( output );
onValueEnd( output );
onFieldEnd( output );
}
catch( Exception e )
{
throw new ValueSerializationException( "Unable to serialize manyassociation " + associationDescriptor, e );
}
} );
descriptor.valueType().namedAssociations().forEach( associationDescriptor -> {
NamedAssociation<?> namedAssociation = state.namedAssociationFor( associationDescriptor.accessor() );
try
{
onFieldStart( output, associationDescriptor.qualifiedName().name() );
onValueStart( output );
onObjectStart( output );
for( String name : namedAssociation )
{
onFieldStart( output, name );
onValueStart( output );
EntityReference ref = namedAssociation.referenceOf( name );
onValue( output, ref.identity() );
onValueEnd( output );
onFieldEnd( output );
}
onObjectEnd( output );
onValueEnd( output );
onFieldEnd( output );
}
catch( Exception e )
{
throw new ValueSerializationException( "Unable to serialize namedassociation " + associationDescriptor, e );
}
} );
onObjectEnd( output );
}
private void serializeEntityComposite( Object object, OutputType output )
throws Exception
{
onValue( output, EntityReference.entityReferenceFor( object ) );
}
private void serializeIterable( Options options, Object object, OutputType output )
throws Exception
{
@SuppressWarnings( "unchecked" )
Iterable<Object> collection = (Iterable<Object>) object;
onArrayStart( output );
for( Object item : collection )
{
onValueStart( output );
doSerialize( options, item, output, false );
onValueEnd( output );
}
onArrayEnd( output );
}
private void serializeMap( Options options, Object object, OutputType output )
throws Exception
{
@SuppressWarnings( "unchecked" )
Map<Object, Object> map = (Map<Object, Object>) object;
//noinspection ConstantConditions
onObjectStart( output );
for( Map.Entry<Object, Object> entry : map.entrySet() )
{
onFieldStart( output, entry.getKey().toString() );
onValueStart( output );
doSerialize( options, entry.getValue(), output, false );
onValueEnd( output );
onFieldEnd( output );
}
onObjectEnd( output );
}
private void serializeBase64Serializable( Object object, OutputType output )
throws Exception
{
onValue( output, serializeBase64Serializable( object ) );
}
private String serializeBase64Serializable( Object object )
throws Exception
{
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream( bout ))
{
out.writeUnshared( object );
}
byte[] bytes = Base64.getEncoder().encode( bout.toByteArray() );
return new String( bytes, UTF_8 );
}
protected abstract OutputType adaptOutput( OutputStream output )
throws Exception;
protected void onSerializationStart( Object object, OutputType output )
throws Exception
{
// NOOP
}
protected void onSerializationEnd( Object object, OutputType output )
throws Exception
{
// NOOP
}
protected abstract void onArrayStart( OutputType output )
throws Exception;
protected abstract void onArrayEnd( OutputType output )
throws Exception;
protected abstract void onObjectStart( OutputType output )
throws Exception;
protected abstract void onObjectEnd( OutputType output )
throws Exception;
protected abstract void onFieldStart( OutputType output, String fieldName )
throws Exception;
protected void onFieldEnd( OutputType output )
throws Exception
{
// NOOP
}
protected void onValueStart( OutputType output )
throws Exception
{
// NOOP
}
protected abstract void onValue( OutputType output, Object value )
throws Exception;
protected void onValueEnd( OutputType output )
throws Exception
{
// NOOP
}
}