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

org.springframework.data.aerospike.convert.MappingAerospikeWriteConverter Maven / Gradle / Ivy

There is a newer version: 4.8.0
Show newest version
/*
 * Copyright 2012-2020 the original author or authors
 *
 * 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 org.springframework.data.aerospike.convert;

import com.aerospike.client.Key;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.aerospike.mapping.AerospikeMappingContext;
import org.springframework.data.aerospike.mapping.AerospikePersistentEntity;
import org.springframework.data.aerospike.mapping.AerospikePersistentProperty;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.EntityWriter;
import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.springframework.data.aerospike.utility.TimeUtils.unixTimeToOffsetInSeconds;

public class MappingAerospikeWriteConverter implements EntityWriter {

    private final TypeMapper> typeMapper;
    private final AerospikeMappingContext mappingContext;
    private final CustomConversions conversions;
    private final GenericConversionService conversionService;

    public MappingAerospikeWriteConverter(TypeMapper> typeMapper,
                                          AerospikeMappingContext mappingContext, CustomConversions conversions,
                                          GenericConversionService conversionService) {
        this.typeMapper = typeMapper;
        this.mappingContext = mappingContext;
        this.conversions = conversions;
        this.conversionService = conversionService;
    }

    private static Collection asCollection(final Object source) {
        if (source instanceof Collection) {
            return (Collection) source;
        }
        return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
    }

    @Override
    public void write(Object source, final AerospikeWriteData data) {
        if (source == null) {
            return;
        }

        boolean hasCustomConverter = conversions.hasCustomWriteTarget(source.getClass(), AerospikeWriteData.class);
        if (hasCustomConverter) {
            convertToAerospikeWriteData(source, data);
            return;
        }

        TypeInformation type = TypeInformation.of(source.getClass());
        AerospikePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass());
        ConvertingPropertyAccessor accessor =
            new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(source), conversionService);

        AerospikePersistentProperty idProperty = entity.getIdProperty();
        if (idProperty != null) {
            String id = accessor.getProperty(idProperty, String.class);
            Assert.notNull(id, "Id must not be null!");

            data.setKey(new Key(data.getKey().namespace, entity.getSetName(), id));
        }

        AerospikePersistentProperty versionProperty = entity.getVersionProperty();
        if (versionProperty != null) {
            Integer version = accessor.getProperty(versionProperty, Integer.class);
            data.setVersion(version);
        }

        data.setExpiration(getExpiration(entity, accessor));

        Map convertedProperties = convertProperties(type, entity, accessor, false);

        if (data.getRequestedBins().isEmpty()) {
            convertedProperties.forEach(data::addBin);
        } else {
            convertedProperties.forEach((key, value) -> {
                if (data.getRequestedBins().contains(key)) {
                    data.addBin(key, value);
                }
            });
        }
    }

    private void convertToAerospikeWriteData(Object source, AerospikeWriteData data) {
        AerospikeWriteData converted = conversionService.convert(source, AerospikeWriteData.class);
        data.setBins(converted.getBins());
        data.setKey(converted.getKey());
        data.setExpiration(converted.getExpiration());
    }

    private Map convertProperties(TypeInformation type, AerospikePersistentEntity entity,
                                                  ConvertingPropertyAccessor accessor, boolean isCustomType) {
        Map target = new HashMap<>();
        typeMapper.writeType(type, target);
        entity.doWithProperties((PropertyHandler) property -> {

            Object value = accessor.getProperty(property);
			/*
				For custom type bins - for example a nested POJO (Person has a friend field which is also a person),
				We want to keep non-writable types (@Id, @Expiration, @Version...) as they are.
				This is not relevant for records, only for custom type bins.
			 */
            if (isNotWritable(property) && !isCustomType) {
                return;
            }
            Object valueToWrite = getValueToWrite(value, property.getTypeInformation());
            if (valueToWrite != null) {
                target.put(property.getFieldName(), valueToWrite);
            }
        });
        return target;
    }

    private boolean isNotWritable(AerospikePersistentProperty property) {
        return property.isIdProperty() || property.isExpirationProperty() || property.isVersionProperty()
            || !property.isWritable();
    }

    private Object getValueToWrite(Object value, TypeInformation type) {
        if (value == null) {
            return null;
        } else if (type == null || conversions.isSimpleType(value.getClass())) {
            return getSimpleValueToWrite(value);
        } else {
            return getNonSimpleValueToWrite(value, type);
        }
    }

    private Object getSimpleValueToWrite(Object value) {
        Optional> customTarget = conversions.getCustomWriteTarget(value.getClass());
        return customTarget
            .map(aClass -> conversionService.convert(value, aClass))
            .orElse(value);
    }

    private Object getNonSimpleValueToWrite(Object value, TypeInformation type) {
        TypeInformation valueType = TypeInformation.of(value.getClass());

        if (valueType.isCollectionLike()) {
            return convertCollection(asCollection(value), type);
        }

        if (valueType.isMap()) {
            return convertMap(asMap(value), type);
        }

        Optional> basicTargetType = conversions.getCustomWriteTarget(value.getClass());
        return basicTargetType
            .map(aClass -> conversionService.convert(value, aClass))
            .orElseGet(() -> convertCustomType(value, valueType));

    }

    private List convertCollection(final Collection source, final TypeInformation type) {
        Assert.notNull(source, "Given collection must not be null!");
        Assert.notNull(type, "Given type must not be null!");

        TypeInformation componentType = type.getComponentType();

        return source.stream().map(element -> getValueToWrite(element, componentType)).collect(Collectors.toList());
    }

    private Map convertMap(final Map source, final TypeInformation type) {
        Assert.notNull(source, "Given map must not be null!");
        Assert.notNull(type, "Given type must not be null!");

        return source.entrySet().stream().collect(HashMap::new, (m, e) -> {
            Object key = e.getKey();
            Object value = e.getValue();
            if (!conversions.isSimpleType(key.getClass())) {
                throw new MappingException("Cannot use a complex object as a key value.");
            }

            String simpleKey;
            if (conversionService.canConvert(key.getClass(), String.class)) {
                simpleKey = conversionService.convert(key, String.class);
            } else {
                simpleKey = key.toString();
            }

            Object convertedValue = getValueToWrite(value, type.getMapValueType());
            m.put(simpleKey, convertedValue);
        }, HashMap::putAll);
    }

    private Map convertCustomType(Object source, TypeInformation type) {
        Assert.notNull(source, "Given map must not be null!");
        Assert.notNull(type, "Given type must not be null!");

        AerospikePersistentEntity entity = mappingContext.getRequiredPersistentEntity(source.getClass());
        ConvertingPropertyAccessor accessor =
            new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(source), conversionService);

        return convertProperties(type, entity, accessor, true);
    }

    @SuppressWarnings("unchecked")
    private Map asMap(Object value) {
        return (Map) value;
    }

    private int getExpiration(AerospikePersistentEntity entity, ConvertingPropertyAccessor accessor) {
        AerospikePersistentProperty expirationProperty = entity.getExpirationProperty();
        if (expirationProperty != null) {
            return getExpirationFromProperty(accessor, expirationProperty);
        }

        return entity.getExpiration();
    }

    private int getExpirationFromProperty(ConvertingPropertyAccessor accessor,
                                          AerospikePersistentProperty expirationProperty) {
        if (expirationProperty.isExpirationSpecifiedAsUnixTime()) {
            Long unixTime = accessor.getProperty(expirationProperty, Long.class);
            Assert.notNull(unixTime, "Expiration must not be null!");
            int inSeconds = unixTimeToOffsetInSeconds(unixTime);
            Assert.isTrue(inSeconds > 0, "Expiration value must be greater than zero, but was: "
                + inSeconds + " seconds (unix time: " + unixTime + ")");
            return inSeconds;
        }

        Integer expirationInSeconds = accessor.getProperty(expirationProperty, Integer.class);
        Assert.notNull(expirationInSeconds, "Expiration must not be null!");

        return expirationInSeconds;
    }
}