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

com.github.nmorel.gwtjackson.rebind.BeanJsonDeserializerCreator Maven / Gradle / Ivy

Go to download

gwt-jackson is a GWT JSON serializer/deserializer mechanism based on Jackson annotations

There is a newer version: 0.15.4
Show newest version
/*
 * 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.rebind;

import javax.annotation.Nullable;
import javax.lang.model.element.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.github.nmorel.gwtjackson.client.JsonDeserializationContext;
import com.github.nmorel.gwtjackson.client.JsonDeserializer;
import com.github.nmorel.gwtjackson.client.JsonDeserializerParameters;
import com.github.nmorel.gwtjackson.client.deser.bean.AbstractBeanJsonDeserializer;
import com.github.nmorel.gwtjackson.client.deser.bean.AbstractIdentityDeserializationInfo;
import com.github.nmorel.gwtjackson.client.deser.bean.AnySetterDeserializer;
import com.github.nmorel.gwtjackson.client.deser.bean.BackReferenceProperty;
import com.github.nmorel.gwtjackson.client.deser.bean.BeanPropertyDeserializer;
import com.github.nmorel.gwtjackson.client.deser.bean.HasDeserializerAndParameters;
import com.github.nmorel.gwtjackson.client.deser.bean.IdentityDeserializationInfo;
import com.github.nmorel.gwtjackson.client.deser.bean.Instance;
import com.github.nmorel.gwtjackson.client.deser.bean.InstanceBuilder;
import com.github.nmorel.gwtjackson.client.deser.bean.PropertyIdentityDeserializationInfo;
import com.github.nmorel.gwtjackson.client.deser.bean.SimpleStringMap;
import com.github.nmorel.gwtjackson.client.deser.bean.SubtypeDeserializer;
import com.github.nmorel.gwtjackson.client.deser.bean.SubtypeDeserializer.BeanSubtypeDeserializer;
import com.github.nmorel.gwtjackson.client.deser.bean.SubtypeDeserializer.DefaultSubtypeDeserializer;
import com.github.nmorel.gwtjackson.client.deser.bean.TypeDeserializationInfo;
import com.github.nmorel.gwtjackson.client.stream.JsonReader;
import com.github.nmorel.gwtjackson.client.stream.JsonToken;
import com.github.nmorel.gwtjackson.rebind.bean.BeanIdentityInfo;
import com.github.nmorel.gwtjackson.rebind.bean.BeanTypeInfo;
import com.github.nmorel.gwtjackson.rebind.exception.UnsupportedTypeException;
import com.github.nmorel.gwtjackson.rebind.property.FieldAccessor;
import com.github.nmorel.gwtjackson.rebind.property.FieldAccessor.Accessor;
import com.github.nmorel.gwtjackson.rebind.property.PropertyInfo;
import com.github.nmorel.gwtjackson.rebind.type.JDeserializerType;
import com.github.nmorel.gwtjackson.rebind.writer.JsniCodeBlockBuilder;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JAbstractMethod;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.thirdparty.guava.common.base.Function;
import com.google.gwt.thirdparty.guava.common.base.Joiner;
import com.google.gwt.thirdparty.guava.common.base.Optional;
import com.google.gwt.thirdparty.guava.common.collect.Collections2;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.findFirstTypeToApplyPropertyAnnotation;
import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.getDefaultValueForType;
import static com.github.nmorel.gwtjackson.rebind.writer.JTypeName.DEFAULT_WILDCARD;
import static com.github.nmorel.gwtjackson.rebind.writer.JTypeName.parameterizedName;
import static com.github.nmorel.gwtjackson.rebind.writer.JTypeName.rawName;
import static com.github.nmorel.gwtjackson.rebind.writer.JTypeName.typeName;

/**
 * @author Nicolas Morel
 */
public class BeanJsonDeserializerCreator extends AbstractBeanJsonCreator {

    private static final String INSTANCE_BUILDER_VARIABLE_FORMAT = "property_%d";

    public static final String DELEGATION_PARAM_NAME = "delegation";

    private static final String INSTANCE_BUILDER_DESERIALIZER_PREFIX = "deserializer_";

    public BeanJsonDeserializerCreator( TreeLogger logger, GeneratorContext context, RebindConfiguration configuration, JacksonTypeOracle
            typeOracle, JClassType beanType ) throws UnableToCompleteException {
        super( logger, context, configuration, typeOracle, beanType );
    }

    @Override
    protected final boolean isSerializer() {
        return false;
    }

    @Override
    protected final void buildSpecific( TypeSpec.Builder typeBuilder ) throws UnableToCompleteException, UnsupportedTypeException {

        if ( beanInfo.getCreatorMethod().isPresent() ) {
            typeBuilder.addMethod( buildInitInstanceBuilderMethod() );
        }

        // no need to generate properties for non instantiable class
        if ( (beanInfo.getCreatorMethod().isPresent() && !beanInfo.isCreatorDelegation()) &&
                (!properties.isEmpty() || beanInfo
                        .getAnySetterPropertyInfo().isPresent()) ) {
            buildInitPropertiesMethods( typeBuilder );
        }

        if ( beanInfo.getIdentityInfo().isPresent() ) {
            try {
                typeBuilder.addMethod( buildInitIdentityInfoMethod( beanInfo.getIdentityInfo().get() ) );
            } catch ( UnsupportedTypeException e ) {
                logger.log( Type.WARN, "Identity type is not supported. We ignore it." );
            }
        }

        if ( beanInfo.getTypeInfo().isPresent() ) {
            typeBuilder.addMethod( buildInitTypeInfoMethod( beanInfo.getTypeInfo().get() ) );
        }

        ImmutableList subtypes = filterSubtypes();
        if ( !subtypes.isEmpty() ) {
            typeBuilder.addMethod( buildInitMapSubtypeClassToDeserializerMethod( subtypes ) );
        }

        if ( beanInfo.isIgnoreUnknown() ) {
            typeBuilder.addMethod( buildIsDefaultIgnoreUnknownMethod() );
        }
    }

    private MethodSpec buildInitInstanceBuilderMethod() throws UnableToCompleteException, UnsupportedTypeException {

        MethodSpec.Builder initInstanceBuilderMethodBuilder = MethodSpec.methodBuilder( "initInstanceBuilder" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( parameterizedName( InstanceBuilder.class, beanInfo.getType() ) );

        TypeName deserializersMapTypeName = ParameterizedTypeName.get( ClassName.get( SimpleStringMap.class ),
                ClassName.get( HasDeserializerAndParameters.class ) );

        if ( null != beanInfo.getCreatorParameters() && !beanInfo.getCreatorParameters().isEmpty() ) {

            initInstanceBuilderMethodBuilder
                    .addStatement( "final $T deserializers = $T.createObject().cast()", deserializersMapTypeName, SimpleStringMap.class );

            // for each constructor parameters, we initialize its deserializer.
            int index = 0;
            for ( Entry entry : beanInfo.getCreatorParameters().entrySet() ) {

                TypeName typeName = typeName( true, entry.getValue().getType() );
                TypeName deserializerTypeName = ParameterizedTypeName.get( ClassName.get( HasDeserializerAndParameters.class ), typeName,
                        ParameterizedTypeName.get( ClassName.get( JsonDeserializer.class ), typeName ) );

                TypeSpec.Builder deserializerBuilder = TypeSpec.anonymousClassBuilder( "" )
                        .superclass( deserializerTypeName );
                List commonMethods = buildCommonPropertyDeserializerMethods( properties.get( entry.getKey() ) );
                for ( MethodSpec method : commonMethods ) {
                    deserializerBuilder.addMethod( method );
                }

                String deserializerName = INSTANCE_BUILDER_DESERIALIZER_PREFIX + String
                        .format( INSTANCE_BUILDER_VARIABLE_FORMAT, index++ );
                initInstanceBuilderMethodBuilder
                        .addStatement( "final $T $L = $L", deserializerTypeName, deserializerName, deserializerBuilder.build() );
                initInstanceBuilderMethodBuilder.addStatement( "deserializers.put($S, $L)", entry.getKey(), deserializerName );
            }
        } else {
            initInstanceBuilderMethodBuilder.addStatement( "final $T deserializers = null", deserializersMapTypeName );
        }
        initInstanceBuilderMethodBuilder.addCode( "\n" );

        return initInstanceBuilderMethodBuilder
                .addStatement( "return $L", buildInstanceBuilderClass() )
                .build();
    }

    private TypeSpec buildInstanceBuilderClass() {
        MethodSpec createMethod = buildInstanceBuilderCreateMethod();

        MethodSpec.Builder newInstanceMethodBuilder = MethodSpec.methodBuilder( "newInstance" )
                .addModifiers( Modifier.PUBLIC )
                .addAnnotation( Override.class )
                .returns( parameterizedName( Instance.class, beanInfo.getType() ) )
                .addParameter( JsonReader.class, "reader" )
                .addParameter( JsonDeserializationContext.class, "ctx" )
                .addParameter( ParameterizedTypeName.get( Map.class, String.class, String.class ), "bufferedProperties" )
                .addParameter( ParameterizedTypeName.get( Map.class, String.class, Object.class ), "bufferedPropertiesValues" );

        if ( beanInfo.isCreatorDefaultConstructor() ) {
            buildNewInstanceMethodForDefaultConstructor( newInstanceMethodBuilder, createMethod );
        } else if ( beanInfo.isCreatorDelegation() ) {
            buildNewInstanceMethodForConstructorOrFactoryMethodDelegation( newInstanceMethodBuilder, createMethod );
        } else {
            buildNewInstanceMethodForConstructorOrFactoryMethod( newInstanceMethodBuilder, createMethod );
        }

        MethodSpec.Builder deserializersGetter = MethodSpec.methodBuilder( "getParametersDeserializer" )
                .addModifiers( Modifier.PUBLIC )
                .addAnnotation( Override.class )
                .addStatement( "return deserializers" )
                .returns( ParameterizedTypeName.get( ClassName.get( SimpleStringMap.class ),
                        ClassName.get( HasDeserializerAndParameters.class ) ) );

        TypeSpec.Builder instanceBuilder = TypeSpec.anonymousClassBuilder( "" )
                .addSuperinterface( parameterizedName( InstanceBuilder.class, beanInfo.getType() ) )
                .addMethod( newInstanceMethodBuilder.build() )
                .addMethod( createMethod )
                .addMethod( deserializersGetter.build() );

        return instanceBuilder.build();
    }

    /**
     * Generate the instance builder class body for a default constructor. We directly instantiate the bean at the builder creation and we
     * set the properties to it
     *
     * @param newInstanceMethodBuilder builder for the
     * {@link InstanceBuilder#newInstance(JsonReader, JsonDeserializationContext, Map, Map)}
     * method
     * @param createMethod the create method
     */
    private void buildNewInstanceMethodForDefaultConstructor( MethodSpec.Builder newInstanceMethodBuilder, MethodSpec createMethod ) {
        newInstanceMethodBuilder.addStatement( "return new $T($N(), bufferedProperties)",
                parameterizedName( Instance.class, beanInfo.getType() ), createMethod );
    }

    /**
     * Generate the instance builder class body for a constructor with parameters or factory method. We will declare all the fields and
     * instanciate the bean only on build() method when all properties have been deserialiazed
     *
     * @param newInstanceMethodBuilder builder for the
     * {@link InstanceBuilder#newInstance(JsonReader, JsonDeserializationContext, Map, Map)} method
     * @param createMethod the create method
     */
    private void buildNewInstanceMethodForConstructorOrFactoryMethod( MethodSpec.Builder newInstanceMethodBuilder,
                                                                      MethodSpec createMethod ) {
        // we don't use directly the property name to name our variable in case it contains invalid character
        ImmutableMap.Builder propertyNameToVariableBuilder = ImmutableMap.builder();

        List requiredProperties = new ArrayList();
        int propertyIndex = 0;
        for ( String name : beanInfo.getCreatorParameters().keySet() ) {
            String variableName = String.format( INSTANCE_BUILDER_VARIABLE_FORMAT, propertyIndex++ );
            propertyNameToVariableBuilder.put( name, variableName );
            PropertyInfo propertyInfo = properties.get( name );

            newInstanceMethodBuilder.addCode( "$T $L = $L; // property '$L'\n",
                    typeName( propertyInfo.getType() ), variableName, getDefaultValueForType( propertyInfo.getType() ), name );

            if ( propertyInfo.isRequired() ) {
                requiredProperties.add( name );
            }
        }
        newInstanceMethodBuilder.addCode( "\n" );
        ImmutableMap propertyNameToVariable = propertyNameToVariableBuilder.build();

        newInstanceMethodBuilder.addStatement( "int nbParamToFind = $L", beanInfo.getCreatorParameters().size() );

        if ( !requiredProperties.isEmpty() ) {
            CodeBlock code = CodeBlock.builder()
                    .add( Joiner.on( ", " ).join( Collections2.transform( requiredProperties, new Function() {
                        @Nullable
                        @Override
                        public Object apply( String s ) {
                            return "$S";
                        }
                    } ) ), requiredProperties.toArray() ).build();

            newInstanceMethodBuilder.addStatement( "$T requiredProperties = new $T($T.asList($L))",
                    ParameterizedTypeName.get( Set.class, String.class ),
                    ParameterizedTypeName.get( HashSet.class, String.class ),
                    Arrays.class, code );
        }

        newInstanceMethodBuilder.addCode( "\n" );

        newInstanceMethodBuilder.beginControlFlow( "if (null != bufferedPropertiesValues)" );
        newInstanceMethodBuilder.addStatement( "Object value" );
        for ( String name : beanInfo.getCreatorParameters().keySet() ) {
            String variableName = propertyNameToVariable.get( name );
            PropertyInfo propertyInfo = properties.get( name );

            newInstanceMethodBuilder.addCode( "\n" );
            newInstanceMethodBuilder.addStatement( "value = bufferedPropertiesValues.remove($S)", name );
            newInstanceMethodBuilder.beginControlFlow( "if (null != value)" );
            newInstanceMethodBuilder.addStatement( "$L = ($T) value", variableName, typeName( true, propertyInfo.getType() ) );
            newInstanceMethodBuilder.addStatement( "nbParamToFind--" );
            if ( propertyInfo.isRequired() ) {
                newInstanceMethodBuilder.addStatement( "requiredProperties.remove($S)", name );
            }
            newInstanceMethodBuilder.endControlFlow();
        }
        newInstanceMethodBuilder.endControlFlow();

        newInstanceMethodBuilder.addCode( "\n" );

        newInstanceMethodBuilder.beginControlFlow( "if (null != bufferedProperties)" );
        newInstanceMethodBuilder.addStatement( "String value" );
        for ( String name : beanInfo.getCreatorParameters().keySet() ) {
            String variableName = propertyNameToVariable.get( name );
            PropertyInfo propertyInfo = properties.get( name );

            newInstanceMethodBuilder.addCode( "\n" );
            newInstanceMethodBuilder.addStatement( "value = bufferedProperties.remove($S)", name );
            newInstanceMethodBuilder.beginControlFlow( "if (null != value)" );
            if ( null != propertyInfo.getType().isPrimitive() ) {
                newInstanceMethodBuilder.addStatement( "$L = ($T) $L.deserialize(ctx.newJsonReader(value), ctx)",
                        variableName, typeName( true, propertyInfo.getType() ), INSTANCE_BUILDER_DESERIALIZER_PREFIX + variableName );
            } else {
                newInstanceMethodBuilder.addStatement( "$L = $L.deserialize(ctx.newJsonReader(value), ctx)",
                        variableName, INSTANCE_BUILDER_DESERIALIZER_PREFIX + variableName );
            }
            newInstanceMethodBuilder.addStatement( "nbParamToFind--" );
            if ( propertyInfo.isRequired() ) {
                newInstanceMethodBuilder.addStatement( "requiredProperties.remove($S)", name );
            }
            newInstanceMethodBuilder.endControlFlow();
        }
        newInstanceMethodBuilder.endControlFlow();

        newInstanceMethodBuilder.addCode( "\n" );

        newInstanceMethodBuilder.addStatement( "String name" );
        newInstanceMethodBuilder.beginControlFlow( "while (nbParamToFind > 0 && $T.NAME == reader.peek())", JsonToken.class );

        newInstanceMethodBuilder.addStatement( "name = reader.nextName()" );
        newInstanceMethodBuilder.addCode( "\n" );

        for ( String name : beanInfo.getCreatorParameters().keySet() ) {
            String variableName = propertyNameToVariable.get( name );
            PropertyInfo propertyInfo = properties.get( name );

            newInstanceMethodBuilder.beginControlFlow( "if ($S.equals(name))", name );
            newInstanceMethodBuilder.addStatement( "$L = $L.deserialize(reader, ctx)",
                    variableName, INSTANCE_BUILDER_DESERIALIZER_PREFIX + variableName );
            newInstanceMethodBuilder.addStatement( "nbParamToFind--" );
            if ( propertyInfo.isRequired() ) {
                newInstanceMethodBuilder.addStatement( "requiredProperties.remove($S)", name );
            }
            newInstanceMethodBuilder.addStatement( "continue" );
            newInstanceMethodBuilder.endControlFlow();

            newInstanceMethodBuilder.addCode( "\n" );
        }

        newInstanceMethodBuilder.beginControlFlow( "if (null == bufferedProperties)" );
        newInstanceMethodBuilder.addStatement( "bufferedProperties = new $T()",
                ParameterizedTypeName.get( HashMap.class, String.class, String.class ) );
        newInstanceMethodBuilder.endControlFlow();
        newInstanceMethodBuilder.addStatement( "bufferedProperties.put(name, reader.nextValue())" );

        newInstanceMethodBuilder.endControlFlow();

        newInstanceMethodBuilder.addCode( "\n" );

        if ( !requiredProperties.isEmpty() ) {
            newInstanceMethodBuilder.beginControlFlow( "if (!requiredProperties.isEmpty())" );
            newInstanceMethodBuilder
                    .addStatement( "throw ctx.traceError(\"Required properties are missing : \" + requiredProperties, reader)" );
            newInstanceMethodBuilder.endControlFlow();
            newInstanceMethodBuilder.addCode( "\n" );
        }

        newInstanceMethodBuilder.addStatement( "return new $T($N($L), bufferedProperties)",
                parameterizedName( Instance.class, beanInfo.getType() ),
                createMethod,
                Joiner.on( ", " ).join( propertyNameToVariable.values() ) );
    }

    /**
     * Generate the instance builder class body for a constructor or factory method with delegation.
     *
     * @param newInstanceMethodBuilder builder for the
     * {@link InstanceBuilder#newInstance(JsonReader, JsonDeserializationContext, Map, Map)}
     * method
     * @param createMethod the create method
     */
    private void buildNewInstanceMethodForConstructorOrFactoryMethodDelegation( MethodSpec.Builder newInstanceMethodBuilder,
                                                                                MethodSpec createMethod ) {
        String param = String.format( "%s%s.deserialize(reader, ctx)", INSTANCE_BUILDER_DESERIALIZER_PREFIX, String
                .format( INSTANCE_BUILDER_VARIABLE_FORMAT, 0 ) );

        newInstanceMethodBuilder.addStatement( "return new $T($N($L), bufferedProperties)",
                parameterizedName( Instance.class, beanInfo.getType() ), createMethod, param );
    }

    private MethodSpec buildInstanceBuilderCreateMethod() {
        JAbstractMethod method = beanInfo.getCreatorMethod().get();

        MethodSpec.Builder builder = MethodSpec.methodBuilder( "create" )
                .addModifiers( Modifier.PRIVATE )
                .returns( typeName( beanInfo.getType() ) );

        StringBuilder parametersNameBuilder = new StringBuilder();
        int index = 0;
        for ( Map.Entry parameterEntry : beanInfo.getCreatorParameters().entrySet() ) {
            if ( index > 0 ) {
                parametersNameBuilder.append( ", " );
            }
            PropertyInfo property = properties.get( parameterEntry.getKey() );

            String variableName = String.format( INSTANCE_BUILDER_VARIABLE_FORMAT, index++ );
            builder.addParameter( typeName( property.getType() ), variableName );
            parametersNameBuilder.append( variableName );
        }
        String parametersName = parametersNameBuilder.toString();

        if ( method.isPrivate() || (!method.isPublic() && !mapperInfo.isSamePackage()) ) {
            // private method, we use jsni
            builder.addModifiers( Modifier.NATIVE );
            builder.addCode( JsniCodeBlockBuilder.builder()
                    .addStatement( "return $L($L)", method.getJsniSignature(), parametersName )
                    .build() );
        } else {
            if ( null != method.isConstructor() ) {
                builder.addStatement( "return new $T($L)", typeName( beanInfo.getType() ), parametersName );
            } else {
                builder.addStatement( "return $T.$L($L)", typeName( beanInfo.getType() ), method.getName(), parametersName );
            }
        }

        return builder.build();
    }

    private void buildInitPropertiesMethods( TypeSpec.Builder typeBuilder ) throws UnableToCompleteException {

        List ignoredProperties = new ArrayList();
        List requiredProperties = new ArrayList();
        Map deserializerProperties = new LinkedHashMap();
        List backReferenceProperties = new ArrayList();

        for ( PropertyInfo property : properties.values() ) {
            if ( null != beanInfo.getCreatorParameters() && beanInfo.getCreatorParameters().containsKey( property.getPropertyName() ) ) {
                // properties used in constructor are deserialized inside instance builder
                continue;
            }

            if ( property.isIgnored() ) {
                ignoredProperties.add( property );
                continue;
            }

            if ( !property.getSetterAccessor().isPresent() ) {
                // there is no setter visible
                continue;
            }

            if ( !property.getBackReference().isPresent() ) {
                try {
                    JDeserializerType deserializerType = getJsonDeserializerFromType( property.getType() );
                    deserializerProperties.put( property, deserializerType );
                    if ( property.isRequired() ) {
                        requiredProperties.add( property );
                    }
                } catch ( UnsupportedTypeException e ) {
                    logger.log( Type.WARN, "Property '" + property.getPropertyName() + "' is ignored" );
                    ignoredProperties.add( property );
                }
            } else {
                backReferenceProperties.add( property );
            }
        }

        if ( !deserializerProperties.isEmpty() ) {
            typeBuilder.addMethod( buildInitDeserializersMethod( deserializerProperties ) );
        }

        if ( !backReferenceProperties.isEmpty() ) {
            typeBuilder.addMethod( buildInitBackReferenceDeserializersMethod( backReferenceProperties ) );
        }

        if ( !ignoredProperties.isEmpty() ) {
            typeBuilder.addMethod( buildInitIgnoredPropertiesMethod( ignoredProperties ) );
        }

        if ( !requiredProperties.isEmpty() ) {
            typeBuilder.addMethod( buildInitRequiredPropertiesMethod( requiredProperties ) );
        }

        if ( beanInfo.getAnySetterPropertyInfo().isPresent() ) {
            Optional method = buildInitAnySetterDeserializerMethod( beanInfo.getAnySetterPropertyInfo().get() );
            if ( method.isPresent() ) {
                typeBuilder.addMethod( method.get() );
            }
        }
    }

    private Optional buildInitAnySetterDeserializerMethod( PropertyInfo anySetterPropertyInfo )
            throws UnableToCompleteException {

        FieldAccessor fieldAccessor = anySetterPropertyInfo.getSetterAccessor().get();
        JType type = fieldAccessor.getMethod().get().getParameterTypes()[1];

        JDeserializerType deserializerType;
        try {
            deserializerType = getJsonDeserializerFromType( type );
        } catch ( UnsupportedTypeException e ) {
            logger.log( Type.WARN, "Method '" + fieldAccessor.getMethod().get()
                    .getName() + "' annotated with @JsonAnySetter has an unsupported type" );
            return Optional.absent();
        }

        return Optional.of( MethodSpec.methodBuilder( "initAnySetterDeserializer" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( ParameterizedTypeName.get(
                        ClassName.get( AnySetterDeserializer.class ), typeName( beanInfo.getType() ), DEFAULT_WILDCARD ) )
                .addStatement( "return $L", buildDeserializer( anySetterPropertyInfo, type, deserializerType ) )
                .build() );
    }

    private MethodSpec buildInitDeserializersMethod( Map properties ) throws UnableToCompleteException {

        TypeName resultType = ParameterizedTypeName.get( ClassName.get( SimpleStringMap.class ),
                ParameterizedTypeName.get( ClassName.get( BeanPropertyDeserializer.class ),
                        typeName( beanInfo.getType() ), DEFAULT_WILDCARD ) );

        MethodSpec.Builder builder = MethodSpec.methodBuilder( "initDeserializers" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( resultType )
                .addStatement( "$T map = $T.createObject().cast()", resultType, SimpleStringMap.class );

        for ( Entry entry : properties.entrySet() ) {
            PropertyInfo property = entry.getKey();
            JDeserializerType deserializerType = entry.getValue();

            builder.addStatement( "map.put($S, $L)",
                    property.getPropertyName(), buildDeserializer( property, property.getType(), deserializerType ) );
        }

        builder.addStatement( "return map" );
        return builder.build();
    }

    private TypeSpec buildDeserializer( PropertyInfo property, JType propertyType, JDeserializerType deserializerType )
            throws UnableToCompleteException {
        final String paramValue = "value";
        final String paramBean = "bean";
        final String paramProperty = "propertyName";

        final Accessor accessor;
        final Class superclass;
        if ( property.isAnySetter() ) {
            accessor = property.getSetterAccessor().get().getAccessor( paramBean, paramProperty, paramValue );
            superclass = AnySetterDeserializer.class;
        } else {
            accessor = property.getSetterAccessor().get().getAccessor( paramBean, paramValue );
            superclass = BeanPropertyDeserializer.class;
        }

        TypeSpec.Builder builder = TypeSpec.anonymousClassBuilder( "" )
                .superclass( ParameterizedTypeName
                        .get( ClassName.get( superclass ), typeName( true, beanInfo.getType() ), rawName( true, propertyType ) ) );

        List commonMethods = buildCommonPropertyDeserializerMethods( property, deserializerType );
        for ( MethodSpec commonMethod : commonMethods ) {
            builder.addMethod( commonMethod );
        }

        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder( "setValue" )
                .addModifiers( Modifier.PUBLIC )
                .addAnnotation( Override.class )
                .addParameter( typeName( beanInfo.getType() ), paramBean );
        if ( property.isAnySetter() ) {
            methodBuilder.addParameter( String.class, paramProperty );
        }
        methodBuilder.addParameter( rawName( true, propertyType ), paramValue )
                .addParameter( JsonDeserializationContext.class, "ctx" )
                .addStatement( "$L", accessor.getAccessor() );

        if ( property.getManagedReference().isPresent() ) {
            methodBuilder.addStatement( "getDeserializer().setBackReference($S, $L, $L, ctx)",
                    property.getManagedReference().get(), paramBean, paramValue );
        }

        builder.addMethod( methodBuilder.build() );

        if ( accessor.getAdditionalMethod().isPresent() ) {
            builder.addMethod( accessor.getAdditionalMethod().get() );
        }

        return builder.build();
    }

    private List buildCommonPropertyDeserializerMethods( PropertyInfo property )
            throws UnableToCompleteException, UnsupportedTypeException {
        return buildCommonPropertyDeserializerMethods( property, getJsonDeserializerFromType( property.getType() ) );
    }

    private List buildCommonPropertyDeserializerMethods( PropertyInfo property, JDeserializerType deserializerType )
            throws UnableToCompleteException {
        List result = new ArrayList();

        result.add( MethodSpec.methodBuilder( "newDeserializer" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( ParameterizedTypeName.get( ClassName.get( JsonDeserializer.class ), DEFAULT_WILDCARD ) )
                .addStatement( "return $L", deserializerType.getInstance() )
                .build() );

        Optional paramMethod = buildPropertyDeserializerParameters( property, deserializerType );
        if ( paramMethod.isPresent() ) {
            result.add( paramMethod.get() );
        }

        return result;
    }

    private Optional buildPropertyDeserializerParameters( PropertyInfo property, JDeserializerType deserializerType )
            throws UnableToCompleteException {

        if ( !property.getFormat().isPresent()
                && !property.getIgnoredProperties().isPresent()
                && !property.getIgnoreUnknown().isPresent()
                && !property.getIdentityInfo().isPresent()
                && !property.getTypeInfo().isPresent() ) {
            // none of the parameter are set so we don't generate the method
            return Optional.absent();
        }

        JClassType annotatedType = findFirstTypeToApplyPropertyAnnotation( deserializerType );

        CodeBlock.Builder paramBuilder = CodeBlock.builder()
                .add( "return new $T()", JsonDeserializerParameters.class )
                .indent()
                .indent();

        buildCommonPropertyParameters( paramBuilder, property );

        if ( property.getIgnoreUnknown().isPresent() ) {
            paramBuilder.add( "\n.setIgnoreUnknown($L)", Boolean.toString( property.getIgnoreUnknown().get() ) );
        }

        if ( property.getIdentityInfo().isPresent() ) {
            try {
                BeanIdentityInfo identityInfo = property.getIdentityInfo().get();
                if ( identityInfo.isIdABeanProperty() ) {
                    paramBuilder.add( "\n.setIdentityInfo($L)", buildPropertyIdentifierDeserializationInfo( annotatedType, identityInfo ) );
                } else {
                    paramBuilder.add( "\n.setIdentityInfo($L)", buildIdentifierDeserializationInfo( annotatedType, identityInfo,
                            getJsonDeserializerFromType( identityInfo.getType().get() ) ) );
                }
            } catch ( UnsupportedTypeException e ) {
                logger.log( Type.WARN, "Identity type is not supported. We ignore it." );
            }
        }

        if ( property.getTypeInfo().isPresent() ) {
            paramBuilder.add( "\n.setTypeInfo($L)", generateTypeInfo( property.getTypeInfo().get() ) );
        }

        paramBuilder.add( ";\n" )
                .unindent()
                .unindent();

        return Optional.of( MethodSpec.methodBuilder( "newParameters" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .addCode( paramBuilder.build() )
                .returns( JsonDeserializerParameters.class )
                .build() );
    }

    private MethodSpec buildInitBackReferenceDeserializersMethod( List properties )
            throws UnableToCompleteException {
        final String paramBean = "bean";
        final String paramReference = "reference";

        TypeName resultType = ParameterizedTypeName.get( ClassName.get( SimpleStringMap.class ),
                ParameterizedTypeName.get( ClassName.get( BackReferenceProperty.class ),
                        typeName( beanInfo.getType() ), DEFAULT_WILDCARD ) );

        MethodSpec.Builder builder = MethodSpec.methodBuilder( "initBackReferenceDeserializers" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( resultType )
                .addStatement( "$T map = $T.createObject().cast()", resultType, SimpleStringMap.class );

        for ( PropertyInfo property : properties ) {

            Accessor accessor = property.getSetterAccessor().get().getAccessor( paramBean, paramReference );

            TypeSpec.Builder anonymBuilder = TypeSpec.anonymousClassBuilder( "" )
                    .superclass( parameterizedName( BackReferenceProperty.class, beanInfo.getType(), property.getType() ) )
                    .addMethod( MethodSpec.methodBuilder( "setBackReference" )
                            .addModifiers( Modifier.PUBLIC )
                            .addAnnotation( Override.class )
                            .addParameter( typeName( beanInfo.getType() ), paramBean )
                            .addParameter( typeName( property.getType() ), paramReference )
                            .addParameter( JsonDeserializationContext.class, "ctx" )
                            .addStatement( "$L", accessor.getAccessor() )
                            .build()
                    );

            if ( accessor.getAdditionalMethod().isPresent() ) {
                anonymBuilder.addMethod( accessor.getAdditionalMethod().get() );
            }

            builder.addStatement( "map.put($S, $L)", property.getBackReference().get(), anonymBuilder.build() );
        }

        builder.addStatement( "return map" );
        return builder.build();
    }

    private MethodSpec buildInitIgnoredPropertiesMethod( List properties ) {
        MethodSpec.Builder builder = MethodSpec.methodBuilder( "initIgnoredProperties" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( ParameterizedTypeName.get( Set.class, String.class ) )
                .addStatement( "$T col = new $T($L)",
                        ParameterizedTypeName.get( HashSet.class, String.class ),
                        ParameterizedTypeName.get( HashSet.class, String.class ),
                        properties.size()
                );
        for ( PropertyInfo property : properties ) {
            builder.addStatement( "col.add($S)", property.getPropertyName() );
        }
        builder.addStatement( "return col" );
        return builder.build();
    }

    private MethodSpec buildInitRequiredPropertiesMethod( List properties ) {
        MethodSpec.Builder builder = MethodSpec.methodBuilder( "initRequiredProperties" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( ParameterizedTypeName.get( Set.class, String.class ) )
                .addStatement( "$T col = new $T($L)",
                        ParameterizedTypeName.get( HashSet.class, String.class ),
                        ParameterizedTypeName.get( HashSet.class, String.class ),
                        properties.size()
                );
        for ( PropertyInfo property : properties ) {
            builder.addStatement( "col.add($S)", property.getPropertyName() );
        }
        builder.addStatement( "return col" );
        return builder.build();
    }

    private MethodSpec buildInitIdentityInfoMethod( BeanIdentityInfo identityInfo )
            throws UnableToCompleteException, UnsupportedTypeException {

        MethodSpec.Builder builder = MethodSpec.methodBuilder( "initIdentityInfo" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( parameterizedName( IdentityDeserializationInfo.class, beanInfo.getType() ) );

        if ( identityInfo.isIdABeanProperty() ) {
            builder.addStatement( "return $L", buildPropertyIdentifierDeserializationInfo( beanInfo.getType(), identityInfo ) );
        } else {
            builder.addStatement( "return $L",
                    buildIdentifierDeserializationInfo( beanInfo.getType(), identityInfo,
                            getJsonDeserializerFromType( identityInfo.getType().get() ) ) );
        }

        return builder.build();
    }

    private CodeBlock buildPropertyIdentifierDeserializationInfo( JClassType type, BeanIdentityInfo identityInfo ) {
        return CodeBlock.builder().
                add( "new $T($S, $T.class, $T.class)", parameterizedName( PropertyIdentityDeserializationInfo.class, type ),
                        identityInfo.getPropertyName(), identityInfo.getGenerator(), identityInfo.getScope() )
                .build();
    }

    private TypeSpec buildIdentifierDeserializationInfo( JClassType type, BeanIdentityInfo identityInfo,
                                                         JDeserializerType deserializerType ) {
        return TypeSpec.anonymousClassBuilder( "$S, $T.class, $T.class",
                identityInfo.getPropertyName(), identityInfo.getGenerator(), identityInfo.getScope() )
                .superclass( parameterizedName( AbstractIdentityDeserializationInfo.class, type, identityInfo.getType().get() ) )
                .addMethod( MethodSpec.methodBuilder( "newDeserializer" )
                        .addModifiers( Modifier.PROTECTED )
                        .addAnnotation( Override.class )
                        .returns( ParameterizedTypeName.get( ClassName.get( JsonDeserializer.class ), DEFAULT_WILDCARD ) )
                        .addStatement( "return $L", deserializerType.getInstance() )
                        .build()
                ).build();
    }

    private MethodSpec buildInitTypeInfoMethod( BeanTypeInfo beanTypeInfo ) throws UnableToCompleteException {
        return MethodSpec.methodBuilder( "initTypeInfo" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( parameterizedName( TypeDeserializationInfo.class, beanInfo.getType() ) )
                .addStatement( "return $L", generateTypeInfo( beanTypeInfo ) )
                .build();
    }

    private MethodSpec buildInitMapSubtypeClassToDeserializerMethod( ImmutableList subtypes )
            throws UnableToCompleteException {

        Class[] mapTypes = new Class[]{Class.class, SubtypeDeserializer.class};
        TypeName resultType = ParameterizedTypeName.get( Map.class, mapTypes );

        MethodSpec.Builder builder = MethodSpec.methodBuilder( "initMapSubtypeClassToDeserializer" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( resultType )
                .addStatement( "$T map = new $T($L)",
                        resultType, ParameterizedTypeName.get( IdentityHashMap.class, mapTypes ), subtypes.size() );

        for ( JClassType subtype : subtypes ) {

            JDeserializerType deserializerType;
            try {
                deserializerType = getJsonDeserializerFromType( subtype, true );
            } catch ( UnsupportedTypeException e ) {
                logger.log( Type.WARN, "Subtype '" + subtype.getQualifiedSourceName() + "' is not supported. We ignore it." );
                continue;
            }

            Class subtypeClass;
            TypeName deserializerClass;
            if ( configuration.getDeserializer( subtype ).isPresent() || null != subtype.isEnum() ) {
                subtypeClass = DefaultSubtypeDeserializer.class;
                deserializerClass = ParameterizedTypeName.get( ClassName.get( JsonDeserializer.class ), DEFAULT_WILDCARD );
            } else {
                subtypeClass = BeanSubtypeDeserializer.class;
                deserializerClass = ParameterizedTypeName.get( ClassName.get( AbstractBeanJsonDeserializer.class ), DEFAULT_WILDCARD );
            }

            TypeSpec subtypeType = TypeSpec.anonymousClassBuilder( "" )
                    .superclass( subtypeClass )
                    .addMethod( MethodSpec.methodBuilder( "newDeserializer" )
                            .addModifiers( Modifier.PROTECTED )
                            .addAnnotation( Override.class )
                            .returns( deserializerClass )
                            .addStatement( "return $L", deserializerType.getInstance() )
                            .build()
                    ).build();

            builder.addStatement( "map.put($T.class, $L)", rawName( subtype ), subtypeType );
        }

        builder.addStatement( "return map" );
        return builder.build();
    }

    private MethodSpec buildIsDefaultIgnoreUnknownMethod() {
        return MethodSpec.methodBuilder( "isDefaultIgnoreUnknown" )
                .addModifiers( Modifier.PROTECTED )
                .addAnnotation( Override.class )
                .returns( boolean.class )
                .addStatement( "return true" )
                .build();
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy