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

org.eclipse.yasson.internal.model.PropertyModel Maven / Gradle / Ivy

/*
 * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

package org.eclipse.yasson.internal.model;

import java.lang.reflect.Type;
import java.util.EnumSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import javax.json.bind.config.PropertyNamingStrategy;
import javax.json.bind.serializer.JsonbSerializer;

import org.eclipse.yasson.internal.AnnotationIntrospector;
import org.eclipse.yasson.internal.JsonbContext;
import org.eclipse.yasson.internal.ReflectionUtils;
import org.eclipse.yasson.internal.components.AdapterBinding;
import org.eclipse.yasson.internal.components.SerializerBinding;
import org.eclipse.yasson.internal.model.customization.PropertyCustomization;
import org.eclipse.yasson.internal.model.customization.PropertyCustomizationBuilder;
import org.eclipse.yasson.internal.serializer.AdaptedObjectSerializer;
import org.eclipse.yasson.internal.serializer.DefaultSerializers;
import org.eclipse.yasson.internal.serializer.JsonbDateFormatter;
import org.eclipse.yasson.internal.serializer.JsonbNumberFormatter;
import org.eclipse.yasson.internal.serializer.SerializerProviderWrapper;
import org.eclipse.yasson.internal.serializer.UserSerializerSerializer;

/**
 * A model for class property.
 * Property is JavaBean alike meta information field / getter / setter of a property in class.
 */
public class PropertyModel implements Comparable {

    /**
     * Field propertyName as in class by java bean convention.
     */
    private final String propertyName;

    /**
     * Calculated name to be used when reading json document.
     */
    private final String readName;

    /**
     * Calculated name to be used when writing json document.
     */
    private final String writeName;

    /**
     * Field propertyType.
     */
    private final Type propertyType;

    /**
     * Model of the class this field belongs to.
     */
    private final ClassModel classModel;
    
    private final Property property;

    /**
     * Customization of this property.
     */
    private final PropertyCustomization customization;

    private final PropertyValuePropagation propagation;

    private final JsonbSerializer propertySerializer;

    private final Type getterMethodType;

    private final Type setterMethodType;
    
    /**
     * Create a new PropertyModel that merges two existing PropertyModel that have identical read/write names.
     * The input PropertyModel objects MUST be equal (a.equals(b) == true)
     * @param a a PropertyModel instance to merge
     * @param b the other PropertyModel instance to merge
     */
    public PropertyModel(PropertyModel a, PropertyModel b) {
        if (!a.equals(b)) {
            throw new IllegalStateException("Property models " + a + " and " + b + " cannot be merged");
        }
        
        // Initial cloning steps
        this.classModel = a.classModel;
        this.propertyName = a.propertyName;
        this.readName = a.readName;
        this.writeName = a.writeName;
        this.propertyType = a.propertyType;
        this.customization = a.customization;
        
        // Merging steps
        this.getterMethodType = a.getterMethodType != null ? a.getterMethodType : b.getterMethodType;
        this.setterMethodType = a.setterMethodType != null ? a.setterMethodType : b.setterMethodType;
        this.property = a.property;
        if (b.property.getField() != null) {
            this.property.setField(b.property.getField());
        }
        if (b.property.getGetter() != null) {
            this.property.setGetter(b.property.getGetter());
        }
        if (b.property.getSetter() != null) {
            this.property.setSetter(b.property.getSetter());
        }
        this.propagation = new ReflectionPropagation(property,
                classModel.getClassCustomization().getPropertyVisibilityStrategy());
        this.propertySerializer = resolveCachedSerializer();
    }

    /**
     * Creates an instance.
     *
     * @param classModel   Class model of declaring class.
     * @param property     Property.
     * @param jsonbContext Context.
     */
    public PropertyModel(ClassModel classModel, Property property, JsonbContext jsonbContext) {
        this.classModel = classModel;
        this.property = property;
        this.propertyName = property.getName();
        this.propertyType = property.getPropertyType();
        this.propagation = new ReflectionPropagation(property,
                                                     classModel.getClassCustomization().getPropertyVisibilityStrategy());
        this.getterMethodType = propagation.isGetterVisible() ? property.getGetterType() : null;
        this.setterMethodType = propagation.isSetterVisible() ? property.getSetterType() : null;
        this.customization = introspectCustomization(property, jsonbContext);
        this.readName = calculateReadWriteName(customization.getJsonReadName(),
                                               jsonbContext.getConfigProperties().getPropertyNamingStrategy());
        this.writeName = calculateReadWriteName(customization.getJsonWriteName(),
                                                jsonbContext.getConfigProperties().getPropertyNamingStrategy());
        this.propertySerializer = resolveCachedSerializer();
    }

    /**
     * Try to cache serializer for this bean property. Only if type cannot be changed during runtime.
     *
     * @return serializer instance to be cached
     */
    @SuppressWarnings("unchecked")
    private JsonbSerializer resolveCachedSerializer() {
        Type serializationType = getPropertySerializationType();
        if (!ReflectionUtils.isResolvedType(serializationType)) {
            return null;
        }
        if (customization.getSerializeAdapterBinding() != null) {
            return new AdaptedObjectSerializer<>(classModel, customization.getSerializeAdapterBinding());
        }
        if (customization.getSerializerBinding() != null) {
            return new UserSerializerSerializer<>(classModel, customization.getSerializerBinding().getJsonbSerializer());
        }

        final Class propertyRawType = ReflectionUtils.getRawType(serializationType);
        final Optional valueSerializerProvider = DefaultSerializers.getInstance()
                .findValueSerializerProvider(propertyRawType);
        if (valueSerializerProvider.isPresent()) {
            return valueSerializerProvider.get().getSerializerProvider().provideSerializer(customization);
        }

        return null;
    }

    /**
     * Returns which type should be used to deserialization.
     *
     * @return deserialization type
     */
    public Type getPropertyDeserializationType() {
        return setterMethodType == null ? propertyType : setterMethodType;
    }

    /**
     * Returns which type should be used to serialization.
     *
     * @return serialization type
     */
    public Type getPropertySerializationType() {
        return getterMethodType == null ? propertyType : getterMethodType;
    }

    private SerializerBinding getUserSerializerBinding(Property property, JsonbContext jsonbContext) {
        final SerializerBinding serializerBinding = jsonbContext.getAnnotationIntrospector().getSerializerBinding(property);
        if (serializerBinding != null) {
            return serializerBinding;
        }
        return jsonbContext.getComponentMatcher().getSerializerBinding(getPropertySerializationType(), null).orElse(null);
    }

    private PropertyCustomization introspectCustomization(Property property, JsonbContext jsonbContext) {
        final AnnotationIntrospector introspector = jsonbContext.getAnnotationIntrospector();
        final PropertyCustomizationBuilder builder = new PropertyCustomizationBuilder();
        //drop all other annotations for transient properties
        EnumSet transientInfo = introspector.getJsonbTransientCategorized(property);
        if (transientInfo.size() != 0) {
            builder.setReadTransient(transientInfo.contains(AnnotationTarget.GETTER));
            builder.setWriteTransient(transientInfo.contains(AnnotationTarget.SETTER));

            if (transientInfo.contains(AnnotationTarget.PROPERTY)) {
                if (!transientInfo.contains(AnnotationTarget.GETTER)) {
                    builder.setReadTransient(true);
                }
                if (!transientInfo.contains(AnnotationTarget.SETTER)) {
                    builder.setWriteTransient(true);
                }
            }

            if (builder.isReadTransient()) {
                introspector.checkTransientIncompatible(property.getFieldElement());
                introspector.checkTransientIncompatible(property.getGetterElement());
            }
            if (builder.isWriteTransient()) {
                introspector.checkTransientIncompatible(property.getFieldElement());
                introspector.checkTransientIncompatible(property.getSetterElement());
            }
        }

        if (!builder.isReadTransient()) {
            builder.setJsonWriteName(introspector.getJsonbPropertyJsonWriteName(property));
            builder.setNillable(introspector.isPropertyNillable(property)
                                        .orElse(classModel.getClassCustomization().isNillable()));
            builder.setSerializerBinding(getUserSerializerBinding(property, jsonbContext));
        }

        if (!builder.isWriteTransient()) {
            builder.setJsonReadName(introspector.getJsonbPropertyJsonReadName(property));
            builder.setDeserializerBinding(introspector.getDeserializerBinding(property));
        }

        final AdapterBinding adapterBinding = jsonbContext.getAnnotationIntrospector().getAdapterBinding(property);
        if (adapterBinding != null) {
            builder.setSerializeAdapter(adapterBinding);
            builder.setDeserializeAdapter(adapterBinding);
        } else {
            builder.setSerializeAdapter(jsonbContext.getComponentMatcher()
                                                .getSerializeAdapterBinding(getPropertySerializationType(), null).orElse(null));
            builder.setDeserializeAdapter(jsonbContext.getComponentMatcher()
                                                  .getDeserializeAdapterBinding(getPropertyDeserializationType(), null)
                                                  .orElse(null));
        }

        introspectDateFormatter(property, introspector, builder, jsonbContext);
        introspectNumberFormatter(property, introspector, builder);
        builder.setImplementationClass(introspector.getImplementationClass(property));

        return builder.buildPropertyCustomization();
    }

    private void introspectDateFormatter(Property property,
                                         AnnotationIntrospector introspector,
                                         PropertyCustomizationBuilder builder,
                                         JsonbContext jsonbContext) {
        /*
         * If @JsonbDateFormat is placed on getter implementation must use this format on serialization.
         * If @JsonbDateFormat is placed on setter implementation must use this format on deserialization.
         * If @JsonbDateFormat is placed on field implementation must use this format on serialization and deserialization.
         *
         * Priority from high to low is getter / setter > field > class > package > global configuration
         */
        Map jsonDateFormatCategorized = introspector
                .getJsonbDateFormatCategorized(property);
        final JsonbDateFormatter configDateFormatter = jsonbContext.getConfigProperties().getConfigDateFormatter();

        if (!builder.isReadTransient()) {
            final JsonbDateFormatter dateFormatter = getTargetForMostPreciseScope(jsonDateFormatCategorized,
                                                                                  AnnotationTarget.GETTER,
                                                                                  AnnotationTarget.PROPERTY,
                                                                                  AnnotationTarget.CLASS);

            builder.setSerializeDateFormatter(dateFormatter != null ? dateFormatter : configDateFormatter);
        }

        if (!builder.isWriteTransient()) {
            final JsonbDateFormatter dateFormatter = getTargetForMostPreciseScope(jsonDateFormatCategorized,
                                                                                  AnnotationTarget.SETTER,
                                                                                  AnnotationTarget.PROPERTY,
                                                                                  AnnotationTarget.CLASS);

            builder.setDeserializeDateFormatter(dateFormatter != null ? dateFormatter : configDateFormatter);
        }
    }

    private void introspectNumberFormatter(Property property,
                                           AnnotationIntrospector introspector,
                                           PropertyCustomizationBuilder builder) {
        /*
         * If @JsonbNumberFormat is placed on getter implementation must use this format on serialization.
         * If @JsonbNumberFormat is placed on setter implementation must use this format on deserialization.
         * If @JsonbNumberFormat is placed on field implementation must use this format on serialization and deserialization.
         *
         * Priority from high to low is getter / setter > field > class > package > global configuration
         */
        Map jsonNumberFormatCategorized = introspector.getJsonNumberFormatter(property);

        if (!builder.isReadTransient()) {
            builder.setSerializeNumberFormatter(getTargetForMostPreciseScope(jsonNumberFormatCategorized,
                                                                             AnnotationTarget.GETTER,
                                                                             AnnotationTarget.PROPERTY,
                                                                             AnnotationTarget.CLASS));
        }

        if (!builder.isWriteTransient()) {
            builder.setDeserializeNumberFormatter(getTargetForMostPreciseScope(jsonNumberFormatCategorized,
                                                                               AnnotationTarget.SETTER,
                                                                               AnnotationTarget.PROPERTY,
                                                                               AnnotationTarget.CLASS));
        }
    }

    /**
     * Pull result for most significant scope defined by order of annotation targets.
     *
     * @param collectedAnnotations all targets
     * @param targets              ordered target types by scope
     */
    private  T getTargetForMostPreciseScope(Map collectedAnnotations, AnnotationTarget... targets) {
        for (AnnotationTarget target : targets) {
            final T result = collectedAnnotations.get(target);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

    /**
     * Gets property's value.
     *
     * @param object object to read property from
     * @return property's value
     */
    public Object getValue(Object object) {
        return propagation.getValue(object);
    }

    /**
     * Sets a property.
     *
     * If not writable (final, transient, static), ignores property.
     *
     * @param object Object to set value in.
     * @param value  Value to set.
     */
    public void setValue(Object object, Object value) {
        if (!isWritable()) {
            return;
        }
        propagation.setValue(object, value);
    }

    /**
     * Property is readable. Based on access policy and java field modifiers.
     *
     * @return true if can be serialized to JSON
     */
    public boolean isReadable() {
        return !customization.isReadTransient() && propagation.isReadable();
    }

    /**
     * Property is writable. Based on access policy and java field modifiers.
     *
     * @return true if can be deserialized from JSON
     */
    public boolean isWritable() {
        return !customization.isWriteTransient() && propagation.isWritable();
    }

    /**
     * Default property name according to Field / Getter / Setter method names.
     * This name is use for identifying properties, for JSON serialization is used customized name
     * which may be derived from default name.
     *
     * @return default name
     */
    public String getPropertyName() {
        return propertyName;
    }

    /**
     * Model of declaring class of this property.
     *
     * @return class model
     */
    public ClassModel getClassModel() {
        return classModel;
    }

    /**
     * Introspected customization of a property.
     *
     * @return immutable property customization
     */
    public PropertyCustomization getCustomization() {
        return customization;
    }

    @Override
    public int compareTo(PropertyModel o) {
        int compare = readName.compareTo(o.readName);
        if (compare == 0) {
            compare = writeName.compareTo(o.writeName);
        }
        return compare;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        PropertyModel other = (PropertyModel) o;
        return Objects.equals(readName, other.readName)
               && Objects.equals(writeName, other.writeName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(readName, writeName);
    }

    /**
     * Gets a name of JSON document property to read this property from.
     *
     * @return Name of JSON document property.
     */
    public String getReadName() {
        return readName;
    }

    public String getWriteName() {
        return writeName;
    }

    /**
     * Gets serializer.
     *
     * @return Serializer.
     */
    public JsonbSerializer getPropertySerializer() {
        return propertySerializer;
    }

    /**
     * If customized by JsonbPropertyAnnotation, than is used, otherwise use strategy to translate.
     * Since this is cached for performance reasons strategy has to be consistent
     * with calculated values for same input.
     */
    private String calculateReadWriteName(String readWriteName, PropertyNamingStrategy strategy) {
        return readWriteName != null ? readWriteName : strategy.translateName(propertyName);
    }

    /**
     * Wrapper object of {@code java.lang.reflect} representations of this javabean property.
     *
     * @return Property model
     */
    public PropertyValuePropagation getPropagation() {
        return propagation;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy