com.github.nmorel.gwtjackson.rebind.bean.BeanProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.github.nmorel.gwtjackson.rebind.bean.BeanProcessor.java

Source

/*
 * Copyright 2014 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.bean;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.UUID;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.ObjectIdGenerator;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.annotation.ObjectIdGenerators.IntSequenceGenerator;
import com.fasterxml.jackson.annotation.ObjectIdGenerators.PropertyGenerator;
import com.fasterxml.jackson.annotation.ObjectIdGenerators.UUIDGenerator;
import com.github.nmorel.gwtjackson.rebind.BeanJsonDeserializerCreator;
import com.github.nmorel.gwtjackson.rebind.CreatorUtils;
import com.github.nmorel.gwtjackson.rebind.JacksonTypeOracle;
import com.github.nmorel.gwtjackson.rebind.RebindConfiguration;
import com.github.nmorel.gwtjackson.rebind.property.PropertiesContainer;
import com.google.gwt.core.ext.TreeLogger;
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.JConstructor;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.thirdparty.guava.common.base.Optional;
import com.google.gwt.thirdparty.guava.common.base.Strings;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;

import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.findFirstEncounteredAnnotationsOnAllHierarchy;
import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.getAnnotation;
import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.isAnnotationPresent;
import static com.github.nmorel.gwtjackson.rebind.CreatorUtils.isObjectOrSerializable;

/**
 * @author Nicolas Morel.
 */
public final class BeanProcessor {

    public static BeanInfo processBean(TreeLogger logger, JacksonTypeOracle typeOracle,
            RebindConfiguration configuration, JClassType beanType) throws UnableToCompleteException {
        BeanInfoBuilder builder = new BeanInfoBuilder();
        builder.setType(beanType);

        if (null != beanType.isGenericType()) {
            builder.setParameterizedTypes(Arrays.<JClassType>asList(beanType.isGenericType().getTypeParameters()));
        }

        determineInstanceCreator(configuration, typeOracle, logger, beanType, builder);

        Optional<JsonAutoDetect> jsonAutoDetect = findFirstEncounteredAnnotationsOnAllHierarchy(configuration,
                beanType, JsonAutoDetect.class);
        if (jsonAutoDetect.isPresent()) {
            builder.setCreatorVisibility(jsonAutoDetect.get().creatorVisibility());
            builder.setFieldVisibility(jsonAutoDetect.get().fieldVisibility());
            builder.setGetterVisibility(jsonAutoDetect.get().getterVisibility());
            builder.setIsGetterVisibility(jsonAutoDetect.get().isGetterVisibility());
            builder.setSetterVisibility(jsonAutoDetect.get().setterVisibility());
        }

        Optional<JsonIgnoreProperties> jsonIgnoreProperties = findFirstEncounteredAnnotationsOnAllHierarchy(
                configuration, beanType, JsonIgnoreProperties.class);
        if (jsonIgnoreProperties.isPresent()) {
            builder.setIgnoredFields(new LinkedHashSet<String>(Arrays.asList(jsonIgnoreProperties.get().value())));
            builder.setIgnoreUnknown(jsonIgnoreProperties.get().ignoreUnknown());
        }

        Optional<JsonPropertyOrder> jsonPropertyOrder = findFirstEncounteredAnnotationsOnAllHierarchy(configuration,
                beanType, JsonPropertyOrder.class);
        builder.setPropertyOrderAlphabetic(jsonPropertyOrder.isPresent() && jsonPropertyOrder.get().alphabetic());
        if (jsonPropertyOrder.isPresent() && jsonPropertyOrder.get().value().length > 0) {
            builder.setPropertyOrderList(Arrays.asList(jsonPropertyOrder.get().value()));
        } else if (!builder.getCreatorParameters().isEmpty()) {
            List<String> propertyOrderList = new ArrayList<String>(builder.getCreatorParameters().keySet());
            builder.setPropertyOrderList(propertyOrderList);
            if (builder.isPropertyOrderAlphabetic()) {
                Collections.sort(propertyOrderList);
            }
        }

        Optional<JsonInclude> jsonInclude = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, beanType,
                JsonInclude.class);
        if (jsonInclude.isPresent()) {
            builder.setInclude(Optional.of(jsonInclude.get().value()));
        }

        builder.setIdentityInfo(processIdentity(logger, typeOracle, configuration, beanType));
        builder.setTypeInfo(processType(logger, typeOracle, configuration, beanType));

        return builder.build();
    }

    /**
     * Look for the method to create a new instance of the bean. If none are found or the bean is abstract or an interface, we considered it
     * as non instantiable.
     *
     * @param typeOracle the oracle
     * @param logger logger
     * @param beanType type to look for constructor
     * @param builder current bean builder
     */
    private static void determineInstanceCreator(RebindConfiguration configuration, JacksonTypeOracle typeOracle,
            TreeLogger logger, JClassType beanType, BeanInfoBuilder builder) {
        if (isObjectOrSerializable(beanType)) {
            return;
        }

        Optional<JClassType> mixinClass = configuration.getMixInAnnotations(beanType);

        List<JClassType> accessors = new ArrayList<JClassType>();
        if (mixinClass.isPresent()) {
            accessors.add(mixinClass.get());
        }
        accessors.add(beanType);

        // Look for a builder class
        Optional<Annotation> jsonDeserialize = CreatorUtils
                .getAnnotation("com.fasterxml.jackson.databind.annotation.JsonDeserialize", accessors);
        if (jsonDeserialize.isPresent()) {
            Optional<JClassType> builderClass = typeOracle.getClassFromJsonDeserializeAnnotation(logger,
                    jsonDeserialize.get(), "builder");
            if (builderClass.isPresent()) {
                builder.setBuilder(builderClass.get());
                return;
            }
        }

        // we search for @JsonCreator annotation
        JConstructor creatorDefaultConstructor = null;
        JConstructor creatorConstructor = null;

        // we keep the list containing the mixin creator and the real creator
        List<? extends JAbstractMethod> creators = Collections.emptyList();

        if (null == beanType.isInterface() && !beanType.isAbstract()) {
            for (JConstructor constructor : beanType.getConstructors()) {
                if (constructor.getParameters().length == 0) {
                    creatorDefaultConstructor = constructor;
                    continue;
                }

                // A constructor is considered as a creator if
                // - he is annotated with JsonCreator and
                //   * all its parameters are annotated with JsonProperty
                //   * or it has only one parameter
                // - or all its parameters are annotated with JsonProperty

                List<JConstructor> constructors = new ArrayList<JConstructor>();
                if (mixinClass.isPresent() && null == mixinClass.get().isInterface()) {
                    JConstructor mixinConstructor = mixinClass.get()
                            .findConstructor(constructor.getParameterTypes());
                    if (null != mixinConstructor) {
                        constructors.add(mixinConstructor);
                    }
                }
                constructors.add(constructor);

                Optional<JsonIgnore> jsonIgnore = getAnnotation(JsonIgnore.class, constructors);
                if (jsonIgnore.isPresent() && jsonIgnore.get().value()) {
                    continue;
                }

                boolean isAllParametersAnnotatedWithJsonProperty = isAllParametersAnnotatedWith(constructors.get(0),
                        JsonProperty.class);
                if ((isAnnotationPresent(JsonCreator.class, constructors)
                        && ((isAllParametersAnnotatedWithJsonProperty)
                                || (constructor.getParameters().length == 1)))
                        || isAllParametersAnnotatedWithJsonProperty) {
                    creatorConstructor = constructor;
                    creators = constructors;
                    break;
                }
            }
        }

        JMethod creatorFactory = null;
        if (null == creatorConstructor) {
            // searching for factory method
            for (JMethod method : beanType.getMethods()) {
                if (method.isStatic()) {

                    List<JMethod> methods = new ArrayList<JMethod>();
                    if (mixinClass.isPresent() && null == mixinClass.get().isInterface()) {
                        JMethod mixinMethod = mixinClass.get().findMethod(method.getName(),
                                method.getParameterTypes());
                        if (null != mixinMethod && mixinMethod.isStatic()) {
                            methods.add(mixinMethod);
                        }
                    }
                    methods.add(method);

                    Optional<JsonIgnore> jsonIgnore = getAnnotation(JsonIgnore.class, methods);
                    if (jsonIgnore.isPresent() && jsonIgnore.get().value()) {
                        continue;
                    }

                    if (isAnnotationPresent(JsonCreator.class, methods) && (method.getParameters().length == 1
                            || isAllParametersAnnotatedWith(methods.get(0), JsonProperty.class))) {
                        creatorFactory = method;
                        creators = methods;
                        break;
                    }
                }
            }
        }

        final Optional<JAbstractMethod> creatorMethod;
        boolean defaultConstructor = false;

        if (null != creatorConstructor) {
            creatorMethod = Optional.<JAbstractMethod>of(creatorConstructor);
        } else if (null != creatorFactory) {
            creatorMethod = Optional.<JAbstractMethod>of(creatorFactory);
        } else if (null != creatorDefaultConstructor) {
            defaultConstructor = true;
            creatorMethod = Optional.<JAbstractMethod>of(creatorDefaultConstructor);
        } else {
            creatorMethod = Optional.absent();
        }

        builder.setCreatorMethod(creatorMethod);
        builder.setCreatorDefaultConstructor(defaultConstructor);

        if (creatorMethod.isPresent() && !defaultConstructor) {
            if (creatorMethod.get().getParameters().length == 1
                    && !isAllParametersAnnotatedWith(creators.get(0), JsonProperty.class)) {
                // delegation constructor
                builder.setCreatorDelegation(true);
                builder.setCreatorParameters(ImmutableMap.of(BeanJsonDeserializerCreator.DELEGATION_PARAM_NAME,
                        creatorMethod.get().getParameters()[0]));
            } else {
                // we want the property name define in the mixin and the parameter defined in the real creator method
                ImmutableMap.Builder<String, JParameter> creatorParameters = ImmutableMap.builder();
                for (int i = 0; i < creatorMethod.get().getParameters().length; i++) {
                    creatorParameters.put(
                            creators.get(0).getParameters()[i].getAnnotation(JsonProperty.class).value(),
                            creators.get(creators.size() - 1).getParameters()[i]);
                }
                builder.setCreatorParameters(creatorParameters.build());
            }
        }
    }

    private static <T extends Annotation> boolean isAllParametersAnnotatedWith(JAbstractMethod method,
            Class<T> annotation) {
        for (JParameter parameter : method.getParameters()) {
            if (!parameter.isAnnotationPresent(annotation)) {
                return false;
            }
        }

        return true;
    }

    private static Optional<BeanIdentityInfo> processIdentity(TreeLogger logger, JacksonTypeOracle typeOracle,
            RebindConfiguration configuration, JClassType type) throws UnableToCompleteException {
        return processIdentity(logger, typeOracle, configuration, type, Optional.<JsonIdentityInfo>absent(),
                Optional.<JsonIdentityReference>absent());
    }

    public static Optional<BeanIdentityInfo> processIdentity(TreeLogger logger, JacksonTypeOracle typeOracle,
            RebindConfiguration configuration, JClassType type, Optional<JsonIdentityInfo> jsonIdentityInfo,
            Optional<JsonIdentityReference> jsonIdentityReference) throws UnableToCompleteException {

        if (!jsonIdentityInfo.isPresent()) {
            jsonIdentityInfo = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, type,
                    JsonIdentityInfo.class);
        }

        if (jsonIdentityInfo.isPresent() && ObjectIdGenerators.None.class != jsonIdentityInfo.get().generator()) {
            if (!jsonIdentityReference.isPresent()) {
                jsonIdentityReference = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, type,
                        JsonIdentityReference.class);
            }

            String propertyName = jsonIdentityInfo.get().property();
            boolean alwaysAsId = jsonIdentityReference.isPresent() && jsonIdentityReference.get().alwaysAsId();
            Class<? extends ObjectIdGenerator<?>> generator = jsonIdentityInfo.get().generator();
            Class<?> scope = jsonIdentityInfo.get().scope();

            BeanIdentityInfo beanIdentityInfo;
            if (generator.isAssignableFrom(PropertyGenerator.class)) {

                beanIdentityInfo = new BeanIdentityInfo(propertyName, alwaysAsId, generator, scope);

            } else {

                JType identityType;
                if (IntSequenceGenerator.class == generator) {
                    identityType = typeOracle.getType(Integer.class.getName());
                } else if (UUIDGenerator.class == generator) {
                    identityType = typeOracle.getType(UUID.class.getName());
                } else {
                    JClassType generatorType = typeOracle.getType(generator.getCanonicalName());
                    JClassType objectIdGeneratorType = generatorType.getSuperclass();
                    while (!objectIdGeneratorType.getQualifiedSourceName()
                            .equals(ObjectIdGenerator.class.getName())) {
                        objectIdGeneratorType = objectIdGeneratorType.getSuperclass();
                    }
                    identityType = objectIdGeneratorType.isParameterized().getTypeArgs()[0];
                }

                beanIdentityInfo = new BeanIdentityInfo(propertyName, alwaysAsId, generator, scope, identityType);

            }
            return Optional.of(beanIdentityInfo);
        }
        return Optional.absent();
    }

    private static Optional<BeanTypeInfo> processType(TreeLogger logger, JacksonTypeOracle typeOracle,
            RebindConfiguration configuration, JClassType type) throws UnableToCompleteException {
        return processType(logger, typeOracle, configuration, type, Optional.<JsonTypeInfo>absent(),
                Optional.<JsonSubTypes>absent());
    }

    public static Optional<BeanTypeInfo> processType(TreeLogger logger, JacksonTypeOracle typeOracle,
            RebindConfiguration configuration, JClassType type, Optional<JsonTypeInfo> jsonTypeInfo,
            Optional<JsonSubTypes> propertySubTypes) throws UnableToCompleteException {

        if (!jsonTypeInfo.isPresent()) {
            jsonTypeInfo = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, type, JsonTypeInfo.class);
            if (!jsonTypeInfo.isPresent()) {
                return Optional.absent();
            }
        }

        Id use = jsonTypeInfo.get().use();
        As include = jsonTypeInfo.get().include();
        String propertyName = jsonTypeInfo.get().property().isEmpty()
                ? jsonTypeInfo.get().use().getDefaultPropertyName()
                : jsonTypeInfo.get().property();

        Optional<JsonSubTypes> typeSubTypes = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, type,
                JsonSubTypes.class);

        // TODO we could do better, we actually extract metadata twice for a lot of classes
        ImmutableMap<JClassType, String> classToSerializationMetadata = extractMetadata(logger, configuration, type,
                jsonTypeInfo, propertySubTypes, typeSubTypes,
                CreatorUtils.filterSubtypesForSerialization(logger, configuration, type));
        ImmutableMap<JClassType, String> classToDeserializationMetadata = extractMetadata(logger, configuration,
                type, jsonTypeInfo, propertySubTypes, typeSubTypes,
                CreatorUtils.filterSubtypesForDeserialization(logger, configuration, type));

        return Optional.of(new BeanTypeInfo(use, include, propertyName, classToSerializationMetadata,
                classToDeserializationMetadata));
    }

    private static ImmutableMap<JClassType, String> extractMetadata(TreeLogger logger,
            RebindConfiguration configuration, JClassType type, Optional<JsonTypeInfo> jsonTypeInfo,
            Optional<JsonSubTypes> propertySubTypes, Optional<JsonSubTypes> typeSubTypes,
            ImmutableList<JClassType> allSubtypes) throws UnableToCompleteException {

        ImmutableMap.Builder<JClassType, String> classToMetadata = ImmutableMap.builder();

        classToMetadata.put(type, extractTypeMetadata(logger, configuration, type, type, jsonTypeInfo.get(),
                propertySubTypes, typeSubTypes, allSubtypes));

        for (JClassType subtype : allSubtypes) {
            classToMetadata.put(subtype, extractTypeMetadata(logger, configuration, type, subtype,
                    jsonTypeInfo.get(), propertySubTypes, typeSubTypes, allSubtypes));
        }
        return classToMetadata.build();
    }

    private static String extractTypeMetadata(TreeLogger logger, RebindConfiguration configuration,
            JClassType baseType, JClassType subtype, JsonTypeInfo typeInfo, Optional<JsonSubTypes> propertySubTypes,
            Optional<JsonSubTypes> baseSubTypes, ImmutableList<JClassType> allSubtypes)
            throws UnableToCompleteException {
        switch (typeInfo.use()) {
        case NAME:
            // we first look the name on JsonSubTypes annotations. Top ones override the bottom ones.
            String name = findNameOnJsonSubTypes(baseType, subtype, allSubtypes, propertySubTypes, baseSubTypes);
            if (null != name && !"".equals(name)) {
                return name;
            }

            // we look if the name is defined on the type with JsonTypeName
            Optional<JsonTypeName> typeName = findFirstEncounteredAnnotationsOnAllHierarchy(configuration, subtype,
                    JsonTypeName.class);
            if (typeName.isPresent() && !Strings.isNullOrEmpty(typeName.get().value())) {
                return typeName.get().value();
            }

            // we use the default name (ie simple name of the class)
            String simpleBinaryName = subtype.getQualifiedBinaryName();
            int indexLastDot = simpleBinaryName.lastIndexOf('.');
            if (indexLastDot != -1) {
                simpleBinaryName = simpleBinaryName.substring(indexLastDot + 1);
            }
            return simpleBinaryName;
        case MINIMAL_CLASS:
            if (!baseType.getPackage().isDefault()) {
                String basePackage = baseType.getPackage().getName();
                if (subtype.getQualifiedBinaryName().startsWith(basePackage + ".")) {
                    return subtype.getQualifiedBinaryName().substring(basePackage.length());
                }
            }
        case CLASS:
            return subtype.getQualifiedBinaryName();
        default:
            logger.log(TreeLogger.Type.ERROR, "JsonTypeInfo.Id." + typeInfo.use() + " is not supported");
            throw new UnableToCompleteException();
        }
    }

    private static String findNameOnJsonSubTypes(JClassType baseType, JClassType subtype,
            ImmutableList<JClassType> allSubtypes, Optional<JsonSubTypes> propertySubTypes,
            Optional<JsonSubTypes> baseSubTypes) {
        JsonSubTypes.Type typeFound = findTypeOnSubTypes(subtype, propertySubTypes);
        if (null != typeFound) {
            return typeFound.name();
        }

        typeFound = findTypeOnSubTypes(subtype, baseSubTypes);
        if (null != typeFound) {
            return typeFound.name();
        }

        if (baseType != subtype) {
            // we look in all the hierarchy
            JClassType type = subtype;
            while (null != type) {
                if (allSubtypes.contains(type)) {
                    JsonSubTypes.Type found = findTypeOnSubTypes(subtype,
                            Optional.fromNullable(type.getAnnotation(JsonSubTypes.class)));
                    if (null != found) {
                        typeFound = found;
                    }
                }
                type = type.getSuperclass();
            }

            if (null != typeFound) {
                return typeFound.name();
            }
        }

        return null;
    }

    private static JsonSubTypes.Type findTypeOnSubTypes(JClassType subtype, Optional<JsonSubTypes> jsonSubTypes) {
        if (jsonSubTypes.isPresent()) {
            for (JsonSubTypes.Type type : jsonSubTypes.get().value()) {
                if (type.value().getName().equals(subtype.getQualifiedBinaryName())) {
                    return type;
                }
            }
        }
        return null;
    }

    /**
     * Process the properties of the bean to find additionnal informations like @JsonValue.
     *
     * @param configuration the configuration
     * @param logger the logger
     * @param typeOracle the oracle
     * @param beanInfo the previous bean information
     * @param properties the properties of the bean
     *
     * @return the new informations about the bean and its properties
     */
    public static BeanInfo processProperties(RebindConfiguration configuration, TreeLogger logger,
            JacksonTypeOracle typeOracle, BeanInfo beanInfo, PropertiesContainer properties) {
        if (!properties.getValuePropertyInfo().isPresent() && !properties.getAnyGetterPropertyInfo().isPresent()
                && !properties.getAnySetterPropertyInfo().isPresent()) {
            return beanInfo;
        }

        BeanInfoBuilder builder = new BeanInfoBuilder(beanInfo);
        builder.setValuePropertyInfo(properties.getValuePropertyInfo());

        if (properties.getValuePropertyInfo().isPresent() && beanInfo.getTypeInfo().isPresent()
                && As.PROPERTY.equals(beanInfo.getTypeInfo().get().getInclude())) {
            // if the bean has type info on property with @JsonValue, we change it to WRAPPER_ARRAY because the value may not be an object
            BeanTypeInfo typeInfo = beanInfo.getTypeInfo().get();
            builder.setTypeInfo(Optional.of(new BeanTypeInfo(typeInfo.getUse(), As.WRAPPER_ARRAY,
                    typeInfo.getPropertyName(), typeInfo.getMapTypeToSerializationMetadata(),
                    typeInfo.getMapTypeToDeserializationMetadata())));
        }

        builder.setAnyGetterPropertyInfo(properties.getAnyGetterPropertyInfo());
        builder.setAnySetterPropertyInfo(properties.getAnySetterPropertyInfo());

        return builder.build();
    }
}