/* * Copyright 2013-2017 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.cassandra.convert; import static org.springframework.data.cassandra.repository.support.BasicMapId.Entry; import static org.springframework.data.cassandra.repository.support.BasicMapId.id; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; import lombok.AllArgsConstructor; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; import org.springframework.data.cassandra.mapping.CassandraType; import org.springframework.data.cassandra.repository.MapId; import org.springframework.data.cassandra.repository.MapIdentifiable; import org.springframework.data.convert.EntityInstantiator; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELContext; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.datastax.driver.core.CodecRegistry; import com.datastax.driver.core.DataType; import com.datastax.driver.core.Row; import com.datastax.driver.core.TypeCodec; import com.datastax.driver.core.UDTValue; import com.datastax.driver.core.UserType; import com.datastax.driver.core.querybuilder.Clause; import com.datastax.driver.core.querybuilder.Delete; import com.datastax.driver.core.querybuilder.Insert; import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Update; /** * {@link CassandraConverter} that uses a {@link MappingContext} to do sophisticated mapping of domain objects to * {@link Row}. * * @author Alex Shvid * @author Matthew T. Adams * @author Oliver Gierke * @author Mark Paluch * @author Antoine Toulme * @author John Blum * @see org.springframework.beans.factory.InitializingBean * @see org.springframework.context.ApplicationContextAware * @see org.springframework.beans.factory.BeanClassLoaderAware * @see org.springframework.data.convert.EntityConverter * @see org.springframework.data.convert.EntityReader * @see org.springframework.data.convert.EntityWriter */ public class MappingCassandraConverter extends AbstractCassandraConverter implements CassandraConverter, ApplicationContextAware, BeanClassLoaderAware { protected ApplicationContext applicationContext; protected final CassandraMappingContext mappingContext; protected ClassLoader beanClassLoader; protected SpELContext spELContext; private final Logger log = LoggerFactory.getLogger(getClass()); /** * Create a new {@link MappingCassandraConverter} with a {@link BasicCassandraMappingContext}. */ public MappingCassandraConverter() { this(new BasicCassandraMappingContext()); } /** * Create a new {@link MappingCassandraConverter} with the given {@link CassandraMappingContext}. * * @param mappingContext must not be {@literal null}. */ public MappingCassandraConverter(CassandraMappingContext mappingContext) { super(new DefaultConversionService()); Assert.notNull(mappingContext, "CassandraMappingContext must not be null"); this.mappingContext = mappingContext; this.spELContext = new SpELContext(RowReaderPropertyAccessor.INSTANCE); } /* (non-Javadoc) * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; this.spELContext = new SpELContext(this.spELContext, applicationContext); } @SuppressWarnings("unchecked") public <R> R readRow(Class<R> type, Row row) { Class<R> beanClassLoaderClass = transformClassToBeanClassLoaderClass(type); TypeInformation<? extends R> typeInfo = ClassTypeInformation.from(beanClassLoaderClass); Class<? extends R> rawType = typeInfo.getType(); if (Row.class.isAssignableFrom(rawType)) { return (R) row; } if (getCustomConversions().hasCustomReadTarget(Row.class, rawType) || getConversionService().canConvert(Row.class, rawType)) { return getConversionService().convert(row, rawType); } if (typeInfo.isCollectionLike() || typeInfo.isMap()) { return getConversionService().convert(row, type); } CassandraPersistentEntity<R> persistentEntity = (CassandraPersistentEntity<R>) getMappingContext().getRequiredPersistentEntity(typeInfo); return readEntityFromRow(persistentEntity, row); } protected <S> S readEntityFromRow(CassandraPersistentEntity<S> entity, Row row) { DefaultSpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(row, spELContext); BasicCassandraRowValueProvider rowValueProvider = new BasicCassandraRowValueProvider(row, expressionEvaluator); PersistentEntityParameterValueProvider<CassandraPersistentProperty> parameterValueProvider = new PersistentEntityParameterValueProvider<>(entity, new MappingAndConvertingValueProvider(rowValueProvider), Optional.empty()); EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); S instance = instantiator.createInstance(entity, parameterValueProvider); readPropertiesFromRow(entity, rowValueProvider, getConvertingAccessor(instance, entity)); return instance; } protected <S> S readEntityFromUdt(CassandraPersistentEntity<S> entity, UDTValue udtValue) { DefaultSpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(udtValue, spELContext); CassandraUDTValueProvider valueProvider = new CassandraUDTValueProvider(udtValue, CodecRegistry.DEFAULT_INSTANCE, expressionEvaluator); PersistentEntityParameterValueProvider<CassandraPersistentProperty> parameterValueProvider = new PersistentEntityParameterValueProvider<>(entity, new MappingAndConvertingValueProvider(valueProvider), Optional.empty()); EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); S instance = instantiator.createInstance(entity, parameterValueProvider); readProperties(entity, valueProvider, getConvertingAccessor(instance, entity)); return instance; } protected void readPropertiesFromRow(CassandraPersistentEntity<?> entity, CassandraRowValueProvider row, PersistentPropertyAccessor propertyAccessor) { readProperties(entity, row, propertyAccessor); } protected void readProperties(CassandraPersistentEntity<?> entity, CassandraValueProvider valueProvider, PersistentPropertyAccessor propertyAccessor) { entity.getPersistentProperties().forEach(property -> MappingCassandraConverter.this.readProperty(entity, property, valueProvider, propertyAccessor)); } protected void readProperty(CassandraPersistentEntity<?> entity, CassandraPersistentProperty property, CassandraValueProvider valueProvider, PersistentPropertyAccessor propertyAccessor) { // if true then skip; property was set in constructor if (entity.isConstructorArgument(property)) { return; } if (property.isCompositePrimaryKey()) { CassandraPersistentEntity<?> keyEntity = property.getCompositePrimaryKeyEntity(); Optional<Object> optionalKey = propertyAccessor.getProperty(property); if (!optionalKey.isPresent()) { optionalKey = Optional.of(instantiatePrimaryKey(keyEntity, property, valueProvider)); } // now recurse on using the key this time optionalKey.ifPresent(key -> readProperties(property.getCompositePrimaryKeyEntity(), valueProvider, getConvertingAccessor(key, keyEntity))); // now that the key's properties have been populated, set the key property on the entity propertyAccessor.setProperty(property, optionalKey); return; } if (!valueProvider.hasProperty(property)) { return; } propertyAccessor.setProperty(property, getReadValue(valueProvider, property)); } @SuppressWarnings("unused") protected Object instantiatePrimaryKey(CassandraPersistentEntity<?> entity, CassandraPersistentProperty keyProperty, PropertyValueProvider<CassandraPersistentProperty> propertyProvider) { return instantiators.getInstantiatorFor(entity).createInstance(entity, new PersistentEntityParameterValueProvider<>(entity, propertyProvider, Optional.empty())); } /* (non-Javadoc) * @see org.springframework.data.convert.EntityReader#read(java.lang.Class, S) */ @Override public <R> R read(Class<R> type, Object row) { if (row instanceof Row) { return readRow(type, (Row) row); } throw new MappingException("Unknown row object " + ObjectUtils.nullSafeClassName(row)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.convert.CassandraConverter#convertToColumnType(java.util.Optional) */ @Override @SuppressWarnings("unchecked") public <T> Optional<Object> convertToColumnType(Optional<T> obj) { return convertToColumnType(obj, obj.map(Object::getClass) .map(ClassTypeInformation::from) .orElse((ClassTypeInformation) ClassTypeInformation.OBJECT)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.convert.CassandraConverter#convertToColumnType(java.util.Optional, org.springframework.data.util.TypeInformation) */ @Override public <T> Optional<Object> convertToColumnType(Optional<T> obj, TypeInformation<?> typeInformation) { Assert.notNull(typeInformation, "TypeInformation must not be null"); return obj.flatMap(object -> { if (object.getClass().isArray()) { return Optional.of(object); } return getWriteValue(obj, typeInformation); }); } @Override public void write(Object source, Object sink) { if (source != null) { Class<?> beanClassLoaderClass = transformClassToBeanClassLoaderClass(source.getClass()); CassandraPersistentEntity<?> entity = getMappingContext().getRequiredPersistentEntity(beanClassLoaderClass); write(source, sink, entity); } } @Override public void write(Object source, Object sink, CassandraPersistentEntity<?> entity) { if (source == null) { return; } if (entity == null) { throw new MappingException("No mapping metadata found for " + source.getClass()); } if (sink instanceof Insert) { writeInsertFromObject(source, (Insert) sink, entity); } else if (sink instanceof Update) { writeUpdateFromObject(source, (Update) sink, entity); } else if (sink instanceof Select.Where) { writeSelectWhereFromObject(source, (Select.Where) sink, entity); } else if (sink instanceof Delete.Where) { writeDeleteWhereFromObject(source, (Delete.Where) sink, entity); } else if (sink instanceof UDTValue) { writeUDTValueWhereFromObject(getConvertingAccessor(source, entity), (UDTValue) sink, entity); } else { throw new MappingException("Unknown write target " + sink.getClass().getName()); } } protected void writeInsertFromObject(final Object object, final Insert insert, CassandraPersistentEntity<?> entity) { writeInsertFromWrapper(getConvertingAccessor(object, entity), insert, entity); } protected void writeInsertFromWrapper(final ConvertingPropertyAccessor accessor, final Insert insert, CassandraPersistentEntity<?> entity) { entity.getPersistentProperties().forEach(property -> { Optional<Object> value = getWriteValue(property, accessor); if (log.isDebugEnabled()) { log.debug("doWithProperties Property.type {}, Property.value {}", property.getType().getName(), value); } if (property.isCompositePrimaryKey()) { if (log.isDebugEnabled()) { log.debug("Property is a compositeKey"); } writeInsertFromWrapper(getConvertingAccessor(value.orElse(null), property.getCompositePrimaryKeyEntity()), insert, property.getCompositePrimaryKeyEntity()); return; } if (!value.isPresent()) { return; } if (log.isDebugEnabled()) { log.debug("Adding insert.value [{}] - [{}]", property.getColumnName().toCql(), value); } insert.value(property.getColumnName().toCql(), value.orElse(null)); }); } protected void writeUpdateFromObject(final Object object, final Update update, CassandraPersistentEntity<?> entity) { writeUpdateFromWrapper(getConvertingAccessor(object, entity), update, entity); } protected void writeUpdateFromWrapper(final ConvertingPropertyAccessor accessor, final Update update, final CassandraPersistentEntity<?> entity) { entity.getPersistentProperties().forEach(property -> { Optional<Object> value = getWriteValue(property, accessor); if (property.isCompositePrimaryKey()) { CassandraPersistentEntity<?> keyEntity = property.getCompositePrimaryKeyEntity(); writeUpdateFromWrapper(getConvertingAccessor(value.orElse(null), keyEntity), update, keyEntity); return; } if (isPrimaryKeyPart(property)) { update.where(QueryBuilder.eq(property.getColumnName().toCql(), value.orElse(null))); } else { update.with(QueryBuilder.set(property.getColumnName().toCql(), value.orElse(null))); } }); } protected void writeSelectWhereFromObject(final Object object, final Select.Where where, CassandraPersistentEntity<?> entity) { getWhereClauses(object, entity).forEach(where::and); } protected void writeDeleteWhereFromObject(final Object object, final Delete.Where where, CassandraPersistentEntity<?> entity) { getWhereClauses(object, entity).forEach(where::and); } protected void writeUDTValueWhereFromObject(final ConvertingPropertyAccessor accessor, final UDTValue udtValue, CassandraPersistentEntity<?> entity) { entity.getPersistentProperties().forEach(property -> { Optional<Object> value = getWriteValue(property, accessor); if (log.isDebugEnabled()) { log.debug("writeUDTValueWhereFromObject Property.type {}, Property.value {}", property.getType().getName(), value); } if (log.isDebugEnabled()) { log.debug("Adding udt.value [{}] - [{}]", property.getColumnName().toCql(), value); } TypeCodec<Object> typeCodec = CodecRegistry.DEFAULT_INSTANCE.codecFor(getMappingContext().getDataType(property)); udtValue.set(property.getColumnName().toCql(), value.orElse(null), typeCodec); }); } private Collection<Clause> getWhereClauses(Object source, CassandraPersistentEntity<?> entity) { Assert.notNull(source, "Id source must not be null"); Object id = extractId(source, entity); Assert.notNull(id, String.format("No Id value found in object %s", source)); Optional<CassandraPersistentProperty> optionalIdProperty = entity.getIdProperty(); Optional<CassandraPersistentProperty> optionalCompositeIdProperty = optionalIdProperty .filter(CassandraPersistentProperty::isCompositePrimaryKey); if (id instanceof MapId) { // FIXME: Generics CassandraPersistentEntity<?> whereEntity = optionalCompositeIdProperty // .map(CassandraPersistentProperty::getCompositePrimaryKeyEntity) // .orElse((CassandraPersistentEntity) entity); return getWhereClauses((MapId) id, whereEntity); } CassandraPersistentProperty idProperty = optionalIdProperty .orElseThrow(() -> new InvalidDataAccessApiUsageException( String.format("Cannot obtain where clauses for entity [%s] using [%s]", entity.getName(), source))); if (optionalCompositeIdProperty.isPresent()) { CassandraPersistentProperty compositeIdProperty = optionalCompositeIdProperty .filter(p -> ClassUtils.isAssignableValue(p.getType(), id)) .orElseThrow(() -> new InvalidDataAccessApiUsageException( String.format("Cannot use [%s] as composite Id for [%s]", id, entity.getName()))); return getWhereClauses(getConvertingAccessor(id, compositeIdProperty.getCompositePrimaryKeyEntity()), compositeIdProperty.getCompositePrimaryKeyEntity()); } Class<?> targetType = getTargetType(idProperty); if (getConversionService().canConvert(id.getClass(), targetType)) { return Collections.singleton(QueryBuilder.eq(idProperty.getColumnName().toCql(), getPotentiallyConvertedSimpleValue(Optional.of(id), (Class<Object>) targetType).orElse(null))); } return Collections.singleton(QueryBuilder.eq(idProperty.getColumnName().toCql(), id)); } private Object extractId(Object source, CassandraPersistentEntity<?> entity) { if (ClassUtils.isAssignableValue(entity.getType(), source)) { return getId(source, entity); } else if (source instanceof MapId) { return source; } else if (source instanceof MapIdentifiable) { return ((MapIdentifiable) source).getMapId(); } return source; } private Collection<Clause> getWhereClauses(final ConvertingPropertyAccessor accessor, CassandraPersistentEntity<?> entity) { Assert.isTrue(entity.isCompositePrimaryKey(), String.format("Entity [%s] is not a composite primary key", entity.getName())); Collection<Clause> clauses = new ArrayList<>(); entity.getPersistentProperties().forEach(property -> { TypeCodec<Object> codec = getCodec(property); Optional<Object> value = accessor.getProperty(property, codec.getJavaType().getRawType()); clauses.add(QueryBuilder.eq(property.getColumnName().toCql(), value.orElse(null))); }); return clauses; } private Collection<Clause> getWhereClauses(MapId id, CassandraPersistentEntity<?> entity) { Assert.notNull(id, "MapId must not be null"); Collection<Clause> clauses = new ArrayList<>(); for (Entry<String, Object> entry : id.entrySet()) { Optional<CassandraPersistentProperty> lookup = entity.getPersistentProperty(entry.getKey()); CassandraPersistentProperty persistentProperty = lookup .orElseThrow(() -> new IllegalArgumentException(String.format( "MapId contains references [%s] that is an unknown property of [%s]", entry.getKey(), entity.getName()))); Optional<Object> writeValue = getWriteValue(Optional.ofNullable(entry.getValue()), persistentProperty.getTypeInformation()); clauses.add(QueryBuilder.eq(persistentProperty.getColumnName().toCql(), writeValue.orElse(null))); } return clauses; } @Override @SuppressWarnings("unchecked") public Object getId(Object object, CassandraPersistentEntity<?> entity) { Assert.notNull(object, "Object instance must not be null"); Assert.notNull(entity, "CassandraPersistentEntity must not be null"); final ConvertingPropertyAccessor accessor = getConvertingAccessor(object, entity); Assert.isTrue(entity.getType().isAssignableFrom(object.getClass()), String.format("Given instance of type [%s] is not of compatible expected type [%s]", object.getClass().getName(), entity.getType().getName())); if (object instanceof MapIdentifiable) { return ((MapIdentifiable) object).getMapId(); } Optional<CassandraPersistentProperty> optionalIdProperty = entity.getIdProperty(); if (optionalIdProperty.isPresent()) { // TODO: NullId CassandraPersistentProperty idProperty = optionalIdProperty.get(); return accessor.getProperty(idProperty, idProperty.isCompositePrimaryKey() ? (Class<Object>) idProperty.getType() : (Class<Object>) getTargetType(idProperty)).orElse(null); } // if the class doesn't have an id property, then it's using MapId final MapId id = id(); entity.getPersistentProperties() // .filter(CassandraPersistentProperty::isPrimaryKeyColumn) // .forEach(property -> { id.with(property.getName(), getWriteValue(property, accessor).orElse(null)); }); return id; } @SuppressWarnings("unchecked") protected <T> Class<T> transformClassToBeanClassLoaderClass(Class<T> entity) { try { return (Class<T>) ClassUtils.forName(entity.getName(), beanClassLoader); } catch (ClassNotFoundException e) { return entity; } catch (LinkageError e) { return entity; } } @Override public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = classLoader; } @Override public CassandraMappingContext getMappingContext() { return mappingContext; } /** * Create a new {@link ConvertingPropertyAccessor} for the given source and entity. * * @param source must not be {@literal null}. * @param entity must not be {@literal null}. * @return a new {@link ConvertingPropertyAccessor} for the given source and entity. */ private ConvertingPropertyAccessor getConvertingAccessor(Object source, CassandraPersistentEntity<?> entity) { PersistentPropertyAccessor propertyAccessor = (source instanceof PersistentPropertyAccessor ? (PersistentPropertyAccessor) source : entity.getPropertyAccessor(source)); return new ConvertingPropertyAccessor(propertyAccessor, getConversionService()); } /** * Returns whether the property is part of the primary key. * * @param property {@link CassandraPersistentProperty} to evaluate. * @return a boolean value indicating whether the given property is party of a primary key. */ private boolean isPrimaryKeyPart(CassandraPersistentProperty property) { return (property.isCompositePrimaryKey() || property.isPrimaryKeyColumn() || property.isIdProperty()); } private Class<?> getTargetType(CassandraPersistentProperty property) { return getCustomConversions().getCustomWriteTarget(property.getType()).orElseGet(() -> { if (property.findAnnotation(CassandraType.class).isPresent()) { return getPropertyTargetType(property); } if (property.isCompositePrimaryKey() || getCustomConversions().isSimpleType(property.getType()) || property.isCollectionLike()) { return property.getType(); } return getPropertyTargetType(property); }); } private Class<?> getPropertyTargetType(CassandraPersistentProperty property) { DataType dataType = getMappingContext().getDataType(property); if (dataType instanceof UserType) { return property.getType(); } TypeCodec<Object> codec = CodecRegistry.DEFAULT_INSTANCE.codecFor(getMappingContext().getDataType(property)); return codec.getJavaType().getRawType(); } /** * Retrieve the value to write for the given {@link CassandraPersistentProperty} from * {@link ConvertingPropertyAccessor} and perform optionally a conversion of collection element types. * * @param property the property. * @param accessor the property accessor * @return the return value, may be {@literal null}. */ @SuppressWarnings("unchecked") private <T> Optional<T> getWriteValue(CassandraPersistentProperty property, ConvertingPropertyAccessor accessor) { return getWriteValue(accessor.getProperty(property, (Class<T>) getTargetType(property)), property.getTypeInformation()); } /** * Retrieve the value from {@code value} applying the given {@link TypeInformation} and perform optionally a * conversion of collection element types. * * @param optional the value, may be {@literal null}. * @param typeInformation the type information. * @return the return value, may be {@literal null}. */ @SuppressWarnings("unchecked") private <I, O> Optional<O> getWriteValue(Optional<I> optional, TypeInformation<?> typeInformation) { if (!optional.isPresent()) { return Optional.empty(); } I value = optional.get(); Class<O> requestedTargetType = Optional.ofNullable(typeInformation) .map(typeInfo -> (Class<O>) typeInfo.getType()) .orElse(null); if (getCustomConversions().hasCustomWriteTarget(value.getClass(), requestedTargetType)) { return Optional.ofNullable((O) getConversionService().convert(value, getCustomConversions().getCustomWriteTarget(value.getClass(), requestedTargetType) .orElse(requestedTargetType))); } if (getCustomConversions().hasCustomWriteTarget(value.getClass())) { return Optional.ofNullable((O) getConversionService().convert(value, getCustomConversions().getCustomWriteTarget(value.getClass()).get())); } if (getCustomConversions().isSimpleType(value.getClass())) { return getPotentiallyConvertedSimpleValue(optional, requestedTargetType); } TypeInformation<?> type = Optional.ofNullable(typeInformation) .orElseGet(() -> ClassTypeInformation.from((Class) value.getClass())); TypeInformation<?> actualType = type.getActualType(); if (value instanceof Collection) { Collection<Object> original = (Collection<Object>) value; Collection<Object> converted = CollectionFactory.createCollection(getCollectionType(type), original.size()); original.stream() .map(element -> convertToColumnType(Optional.ofNullable(element), actualType).orElse(null)) .forEach(converted::add); return Optional.of((O) converted); } Optional<CassandraPersistentEntity<?>> optionalUdt = getMappingContext() .getPersistentEntity(actualType.getType()) .filter(CassandraPersistentEntity::isUserDefinedType); if (optionalUdt.isPresent()) { return optionalUdt.map(persistentEntity -> { UDTValue udtValue = persistentEntity.getUserType().newValue(); write(value, udtValue, persistentEntity); return (O) udtValue; }); } return (Optional<O>) optional; } /** * Performs special enum handling or simply returns the value as is. * * @param optionalValue may be {@literal null}. * @param requestedTargetType must not be {@literal null}. * @see CassandraType */ @SuppressWarnings("unchecked") private <I, O> Optional<O> getPotentiallyConvertedSimpleValue(Optional<I> optionalValue, Class<O> requestedTargetType) { if (optionalValue.isPresent()) { Object value = optionalValue.get(); // Cassandra has no default enum handling - convert it to either a String // or, if requested, to a different type if (Enum.class.isAssignableFrom(value.getClass())) { if (requestedTargetType != null && !requestedTargetType.isEnum() && getConversionService().canConvert(value.getClass(), requestedTargetType)) { return Optional.ofNullable(getConversionService().convert(value, requestedTargetType)); } return Optional.of((O) ((Enum<?>) value).name()); } } return (Optional<O>) optionalValue; } /** * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies * {@link Enum} handling or returns the value as is. * * @param value simple value to convert into a value of type {@code target}. * @param target must not be {@literal null}. * @return the converted value. */ @SuppressWarnings({ "rawtypes", "unchecked" }) private Object getPotentiallyConvertedSimpleRead(Object value, Class<?> target) { if (value == null || target == null || target.isAssignableFrom(value.getClass())) { return value; } if (getCustomConversions().hasCustomReadTarget(value.getClass(), target)) { return getConversionService().convert(value, target); } if (Enum.class.isAssignableFrom(target)) { return Enum.valueOf((Class<Enum>) target, value.toString()); } return getConversionService().convert(value, target); } private Class<?> getCollectionType(TypeInformation<?> type) { if (type.getType().isInterface()) { return type.getType(); } if (ClassTypeInformation.LIST.isAssignableFrom(type)) { return ClassTypeInformation.LIST.getType(); } if (ClassTypeInformation.SET.isAssignableFrom(type)) { return ClassTypeInformation.SET.getType(); } if (!type.isCollectionLike()) { return ClassTypeInformation.LIST.getType(); } return type.getType(); } /** * Retrieve the value to read for the given {@link CassandraPersistentProperty} from * {@link BasicCassandraRowValueProvider} and perform optionally a conversion of collection element types. * * @param row the row. * @param property the property. * @return the return value, may be {@literal null}. */ @SuppressWarnings("unchecked") private <T> Optional<T> getReadValue(PropertyValueProvider<CassandraPersistentProperty> row, CassandraPersistentProperty property) { Optional<Object> obj = row.getPropertyValue(property); if (!obj.isPresent()) { return Optional.empty(); } if (getCustomConversions().hasCustomWriteTarget(property.getActualType()) && property.isCollectionLike()) { if (obj.filter(it -> it instanceof Collection).isPresent()) { return obj.map(it -> { Collection<Object> original = (Collection<Object>) it; Collection<Object> converted = CollectionFactory.createCollection(property.getType(), original.size()); for (Object element : original) { converted.add(getConversionService().convert(element, property.getActualType())); } return (T) converted; }); } } if (property.isCollectionLike() && obj.filter(it -> it instanceof Collection).isPresent()) { return obj.map(it -> (T) readCollectionOrArray(property.getTypeInformation(), (Collection) it)); } Optional<CassandraPersistentEntity<?>> persistentEntity = getMappingContext() .getPersistentEntity(property.getActualType()).filter(CassandraPersistentEntity::isUserDefinedType); if (persistentEntity.isPresent() && obj.filter(it -> it instanceof UDTValue).isPresent()) { return persistentEntity.flatMap( cassandraPersistentEntity -> obj.map(it -> (T) readEntityFromUdt(cassandraPersistentEntity, (UDTValue) it))); } return obj.flatMap(it -> Optional.of((T) getPotentiallyConvertedSimpleRead(it, property.getType()))); } /** * Reads the given {@link Collection} into a collection of the given {@link TypeInformation}. * * @param targetType must not be {@literal null}. * @param sourceValue must not be {@literal null}. * @return the converted {@link Collection} or array, will never be {@literal null}. */ @SuppressWarnings({ "rawtypes", "unchecked" }) private Object readCollectionOrArray(TypeInformation<?> targetType, Collection<?> sourceValue) { Assert.notNull(targetType, "Target type must not be null!"); Class<?> collectionType = targetType.getType(); Optional<TypeInformation<?>> componentType = targetType.getComponentType(); Class<?> rawComponentType = componentType.map(TypeInformation::getType).orElse((Class) List.class); collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType : List.class; Collection<Object> items = targetType.getType().isArray() ? new ArrayList<Object>() : CollectionFactory.createCollection(collectionType, rawComponentType, sourceValue.size()); if (sourceValue.isEmpty()) { return getPotentiallyConvertedSimpleRead(items, collectionType); } Optional<CassandraPersistentEntity<?>> cassandraPersistentEntity = componentType .flatMap(it -> getMappingContext().getPersistentEntity(it)) .filter(CassandraPersistentEntity::isUserDefinedType); if (cassandraPersistentEntity.isPresent()) { cassandraPersistentEntity.ifPresent(persistentEntity -> { for (Object udtValue : sourceValue) { items.add(readEntityFromUdt(persistentEntity, (UDTValue) udtValue)); } }); } else { for (Object item : sourceValue) { items.add(getPotentiallyConvertedSimpleRead(item, rawComponentType)); } } return getPotentiallyConvertedSimpleRead(items, targetType.getType()); } private TypeCodec<Object> getCodec(CassandraPersistentProperty property) { return CodecRegistry.DEFAULT_INSTANCE.codecFor(mappingContext.getDataType(property)); } /** * {@link CassandraRowValueProvider} that delegates reads to {@link CassandraValueProvider} applying mapping and * custom conversion from {@link MappingCassandraConverter}. * * @author Mark Paluch * @since 1.5.1 */ @AllArgsConstructor class MappingAndConvertingValueProvider implements CassandraValueProvider { private final CassandraValueProvider parent; /* (non-Javadoc) * @see org.springframework.data.cassandra.convert.CassandraValueProvider#hasProperty(org.springframework.data.cassandra.mapping.CassandraPersistentProperty) */ @Override public boolean hasProperty(CassandraPersistentProperty property) { return parent.hasProperty(property); } /* (non-Javadoc) * @see org.springframework.data.mapping.model.PropertyValueProvider#getPropertyValue(org.springframework.data.mapping.PersistentProperty) */ @Override public <T> Optional<T> getPropertyValue(CassandraPersistentProperty property) { return getReadValue(parent, property); } } }