/*
* 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.mapping;
import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId;
import static org.springframework.cassandra.core.keyspace.CreateTableSpecification.createTable;
import static org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder.getDataTypeFor;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.cassandra.core.cql.CqlIdentifier;
import org.springframework.cassandra.core.keyspace.CreateTableSpecification;
import org.springframework.cassandra.core.keyspace.CreateUserTypeSpecification;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.cassandra.convert.CassandraCustomConversions;
import org.springframework.data.cassandra.mapping.UserTypeUtil.FrozenLiteralDataType;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.context.AbstractMappingContext;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import com.datastax.driver.core.DataType;
import com.datastax.driver.core.TableMetadata;
import com.datastax.driver.core.UserType;
/**
* Default implementation of a {@link MappingContext} for Cassandra using {@link CassandraPersistentEntity} and
* {@link CassandraPersistentProperty} as primary abstractions.
*
* @author Alex Shvid
* @author Matthew T. Adams
* @author Mark Paluch
* @author John Blum
* @author Jens Schauder
*/
public class BasicCassandraMappingContext
extends AbstractMappingContext<CassandraPersistentEntity<?>, CassandraPersistentProperty>
implements CassandraMappingContext, ApplicationContextAware {
private CassandraPersistentEntityMetadataVerifier verifier = new CompositeCassandraPersistentEntityMetadataVerifier();
private CustomConversions customConversions;
private Mapping mapping = new Mapping();
private UserTypeResolver userTypeResolver;
private ApplicationContext context;
private ClassLoader beanClassLoader;
// useful caches
private final Map<Class<?>, CassandraPersistentEntity<?>> entitiesByType = new HashMap<>();
private final Map<CqlIdentifier, Set<CassandraPersistentEntity<?>>> entitySetsByTableName = new HashMap<>();
private final Set<CassandraPersistentEntity<?>> primaryKeyEntities = new HashSet<>();
private final Set<CassandraPersistentEntity<?>> userDefinedTypes = new HashSet<>();
private final Set<CassandraPersistentEntity<?>> tableEntities = new HashSet<>();
/**
* Create a new {@link BasicCassandraMappingContext}.
*/
public BasicCassandraMappingContext() {
setCustomConversions(new CassandraCustomConversions(Collections.EMPTY_LIST));
setSimpleTypeHolder(CassandraSimpleTypeHolder.HOLDER);
}
/* (non-Javadoc)
* @see org.springframework.data.mapping.context.AbstractMappingContext#initialize()
*/
@Override
public void initialize() {
super.initialize();
processMappingOverrides();
}
@SuppressWarnings("all")
protected void processMappingOverrides() {
mapping.getEntityMappings().stream()//
.filter(entityMapping -> entityMapping != null).forEach(entityMapping -> {
String entityClassName = entityMapping.getEntityClassName();
try {
Class<?> entityClass = ClassUtils.forName(entityClassName, beanClassLoader);
CassandraPersistentEntity<?> entity = getRequiredPersistentEntity(entityClass);
String entityTableName = entityMapping.getTableName();
if (StringUtils.hasText(entityTableName)) {
entity.setTableName(cqlId(entityTableName, Boolean.valueOf(entityMapping.getForceQuote())));
}
processMappingOverrides(entity, entityMapping);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(String.format("Unknown persistent entity name [%s]", entityClassName), e);
}
});
}
protected void processMappingOverrides(CassandraPersistentEntity<?> entity, EntityMapping entityMapping) {
entityMapping.getPropertyMappings()
.forEach((key, propertyMapping) -> processMappingOverride(entity, propertyMapping));
}
protected void processMappingOverride(CassandraPersistentEntity<?> entity, PropertyMapping mapping) {
CassandraPersistentProperty property = entity.getRequiredPersistentProperty(mapping.getPropertyName());
boolean forceQuote = Boolean.valueOf(mapping.getForceQuote());
property.setForceQuote(forceQuote);
if (StringUtils.hasText(mapping.getColumnName())) {
property.setColumnName(cqlId(mapping.getColumnName(), forceQuote));
}
}
/* (non-Javadoc)
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
public void setBeanClassLoader(ClassLoader beanClassLoader) {
this.beanClassLoader = beanClassLoader;
}
/**
* Sets the {@link CustomConversions}.
*
* @param customConversions must not be {@literal null}.
* @since 1.5
*/
public void setCustomConversions(CustomConversions customConversions) {
Assert.notNull(customConversions, "CustomConversions must not be null");
this.customConversions = customConversions;
}
public void setMapping(Mapping mapping) {
Assert.notNull(mapping, "Mapping must not be null");
this.mapping = mapping;
}
/**
* Sets the {@link UserTypeResolver}.
*
* @param userTypeResolver must not be {@literal null}.
* @since 1.5
*/
public void setUserTypeResolver(UserTypeResolver userTypeResolver) {
Assert.notNull(userTypeResolver, "UserTypeResolver must not be null");
this.userTypeResolver = userTypeResolver;
}
/**
* @param verifier The verifier to set.
*/
@Override
public void setVerifier(CassandraPersistentEntityMetadataVerifier verifier) {
this.verifier = verifier;
}
/**
* @return Returns the verifier.
*/
@SuppressWarnings("unused")
public CassandraPersistentEntityMetadataVerifier getVerifier() {
return verifier;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getNonPrimaryKeyEntities()
*/
@Override
public Collection<CassandraPersistentEntity<?>> getNonPrimaryKeyEntities() {
return getTableEntities();
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getPrimaryKeyEntities()
*/
@Override
public Collection<CassandraPersistentEntity<?>> getPrimaryKeyEntities() {
return Collections.unmodifiableSet(primaryKeyEntities);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getTableEntities()
*/
@Override
public Collection<CassandraPersistentEntity<?>> getTableEntities() {
return Collections.unmodifiableCollection(tableEntities);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getUserDefinedTypeEntities()
*/
@Override
public Collection<CassandraPersistentEntity<?>> getUserDefinedTypeEntities() {
return Collections.unmodifiableSet(userDefinedTypes);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getPersistentEntities(boolean)
*/
@Override
public Collection<CassandraPersistentEntity<?>> getPersistentEntities(boolean includePrimaryKeyTypesAndUdts) {
if (includePrimaryKeyTypesAndUdts) {
return super.getPersistentEntities();
}
return getTableEntities();
}
/* (non-Javadoc)
* @see org.springframework.data.mapping.context.AbstractMappingContext#addPersistentEntity(org.springframework.data.util.TypeInformation)
*/
@Override
protected Optional<CassandraPersistentEntity<?>> addPersistentEntity(TypeInformation<?> typeInformation) {
// Prevent conversion types created as CassandraPersistentEntity
Optional<CassandraPersistentEntity<?>> optional = shouldCreatePersistentEntityFor(typeInformation)
? super.addPersistentEntity(typeInformation) : Optional.empty();
optional.ifPresent(entity -> {
if (entity.isUserDefinedType()) {
userDefinedTypes.add(entity);
}
// now do some caching of the entity
Set<CassandraPersistentEntity<?>> entities = entitySetsByTableName.computeIfAbsent(entity.getTableName(),
cqlIdentifier -> new HashSet<>());
entities.add(entity);
if (!entity.isUserDefinedType()) {
if (entity.isCompositePrimaryKey()) {
primaryKeyEntities.add(entity);
}
entity.findAnnotation(Table.class).ifPresent(table -> tableEntities.add(entity));
}
entitiesByType.put(entity.getType(), entity);
});
return optional;
}
/* (non-Javadoc)
* @see org.springframework.data.mapping.context.AbstractMappingContext#shouldCreatePersistentEntityFor(org.springframework.data.util.TypeInformation)
*/
@Override
protected boolean shouldCreatePersistentEntityFor(TypeInformation<?> typeInfo) {
return (!customConversions.hasCustomWriteTarget(typeInfo.getType())
&& super.shouldCreatePersistentEntityFor(typeInfo));
}
/* (non-Javadoc)
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation)
*/
@Override
protected <T> CassandraPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
UserDefinedType userDefinedType = AnnotatedElementUtils.findMergedAnnotation(typeInformation.getType(),
UserDefinedType.class);
CassandraPersistentEntity<T> entity;
if (userDefinedType != null) {
entity = new CassandraUserTypePersistentEntity<>(typeInformation, this, verifier, userTypeResolver);
} else {
entity = new BasicCassandraPersistentEntity<>(typeInformation, this, verifier);
}
if (context != null) {
entity.setApplicationContext(context);
}
return entity;
}
/* (non-Javadoc)
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentProperty(java.lang.reflect.Field, java.beans.PropertyDescriptor, org.springframework.data.mapping.model.MutablePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder)
*/
@Override
protected CassandraPersistentProperty createPersistentProperty(Property property, CassandraPersistentEntity<?> owner,
SimpleTypeHolder simpleTypeHolder) {
return new BasicCassandraPersistentProperty(property, owner, simpleTypeHolder, userTypeResolver);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#usesTable(com.datastax.driver.core.TableMetadata)
*/
@Override
public boolean usesTable(TableMetadata table) {
return entitySetsByTableName.containsKey(cqlId(table.getName()));
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#usesUserType(com.datastax.driver.core.UserType)
*/
@Override
public boolean usesUserType(final UserType userType) {
CqlIdentifier identifier = CqlIdentifier.cqlId(userType.getTypeName());
return (hasMappedUserType(identifier) || hasReferencedUserType(identifier));
}
private boolean hasReferencedUserType(final CqlIdentifier identifier) {
return getPersistentEntities().stream() //
.flatMap(PersistentEntity::getPersistentProperties) //
.map(it -> it.findAnnotation(CassandraType.class)) //
.filter(Optional::isPresent) //
.flatMap(Optionals::toStream) //
.anyMatch(it -> {
return StringUtils.hasText(it.userTypeName()) //
&& CqlIdentifier.cqlId(it.userTypeName()).equals(identifier);
}); //
}
private boolean hasMappedUserType(CqlIdentifier identifier) {
return userDefinedTypes.stream().map(CassandraPersistentEntity::getTableName).anyMatch(identifier::equals);
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getCreateTableSpecificationFor(org.springframework.data.cassandra.mapping.CassandraPersistentEntity)
*/
@Override
public CreateTableSpecification getCreateTableSpecificationFor(CassandraPersistentEntity<?> entity) {
Assert.notNull(entity, "CassandraPersistentEntity must not be null");
final CreateTableSpecification specification = createTable().name(entity.getTableName());
entity.getPersistentProperties().filter(CassandraPersistentProperty::isCompositePrimaryKey).forEach(property -> {
CassandraPersistentEntity<?> primaryKeyEntity = getRequiredPersistentEntity(property.getRawType());
primaryKeyEntity.getPersistentProperties().forEach(primaryKeyProperty -> {
if (primaryKeyProperty.isPartitionKeyColumn()) {
specification.partitionKeyColumn(primaryKeyProperty.getColumnName(), getDataType(primaryKeyProperty));
} else { // it's a cluster column
specification.clusteredKeyColumn(primaryKeyProperty.getColumnName(), getDataType(primaryKeyProperty),
primaryKeyProperty.getPrimaryKeyOrdering());
}
});
});
entity.getPersistentProperties().filter((property) -> !property.isCompositePrimaryKey()).forEach(property -> {
if (property.isIdProperty() || property.isPartitionKeyColumn()) {
specification.partitionKeyColumn(property.getColumnName(),
UserTypeUtil.potentiallyFreeze(getDataType(property)));
} else if (property.isClusterKeyColumn()) {
specification.clusteredKeyColumn(property.getColumnName(),
UserTypeUtil.potentiallyFreeze(getDataType(property)), property.getPrimaryKeyOrdering());
} else {
specification.column(property.getColumnName(), UserTypeUtil.potentiallyFreeze(getDataType(property)));
}
});
if (specification.getPartitionKeyColumns().isEmpty()) {
throw new MappingException(String.format("No partition key columns found in entity [%s]", entity.getType()));
}
return specification;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getCreateUserTypeSpecificationFor(org.springframework.data.cassandra.mapping.CassandraPersistentEntity)
*/
@Override
public CreateUserTypeSpecification getCreateUserTypeSpecificationFor(CassandraPersistentEntity<?> entity) {
Assert.notNull(entity, "CassandraPersistentEntity must not be null");
final CreateUserTypeSpecification specification = CreateUserTypeSpecification.createType(entity.getTableName());
entity.doWithProperties((PropertyHandler<CassandraPersistentProperty>) property -> {
// Use frozen literal to not resolve types from Cassandra.
// At this stage, they might be not created yet.
specification.field(property.getColumnName(),
getDataTypeWithUserTypeFactory(property, DataTypeProvider.FrozenLiteral));
});
if (specification.getFields().isEmpty()) {
throw new MappingException(String.format("No fields in user type [%s]", entity.getType()));
}
return specification;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getDataType(org.springframework.data.cassandra.mapping.CassandraPersistentProperty)
*/
@Override
public DataType getDataType(CassandraPersistentProperty property) {
return getDataTypeWithUserTypeFactory(property, DataTypeProvider.EntityUserType);
}
private DataType getDataTypeWithUserTypeFactory(CassandraPersistentProperty property,
DataTypeProvider dataTypeProvider) {
if (property.isCompositePrimaryKey()) {
return property.getDataType();
}
if (property.findAnnotation(CassandraType.class).isPresent()) {
return property.getDataType();
}
Optional<CassandraPersistentEntity<?>> persistentEntity = getPersistentEntity(property.getActualType());
if (persistentEntity.filter(CassandraPersistentEntity::isUserDefinedType).isPresent()) {
Optional<DataType> dataType = persistentEntity.map(it -> getUserDataType(property, dataTypeProvider, it));
if (dataType.isPresent()) {
return dataType.get();
}
}
return customConversions.getCustomWriteTarget(property.getType()) //
.map(CassandraSimpleTypeHolder::getDataTypeFor) //
.orElseGet(() -> customConversions.getCustomWriteTarget(property.getActualType()) //
.map(it -> {
if (property.isCollectionLike()) {
if (List.class.isAssignableFrom(property.getType())) {
return DataType.list(getDataTypeFor(it));
}
if (Set.class.isAssignableFrom(property.getType())) {
return DataType.set(getDataTypeFor(it));
}
}
return getDataTypeFor(it);
}).orElseGet(property::getDataType)
);
}
private DataType getUserDataType(CassandraPersistentProperty property, DataTypeProvider dataTypeProvider,
CassandraPersistentEntity<?> persistentEntity) {
DataType elementType = dataTypeProvider.getDataType(persistentEntity);
if (property.isCollectionLike()) {
if (Set.class.isAssignableFrom(property.getType())) {
return DataType.set(elementType);
}
if (List.class.isAssignableFrom(property.getType())) {
return DataType.list(elementType);
}
}
if (!property.isCollectionLike() && !property.isMapLike()) {
return elementType;
}
return null;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getDataType(java.lang.Class)
*/
@Override
public DataType getDataType(Class<?> type) {
return customConversions.getCustomWriteTarget(type) //
.map(CassandraSimpleTypeHolder::getDataTypeFor) //
.orElseGet(() -> getDataTypeFor(type));
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getExistingPersistentEntity(java.lang.Class)
*/
@Override
public CassandraPersistentEntity<?> getExistingPersistentEntity(Class<?> type) {
CassandraPersistentEntity<?> entity = entitiesByType.get(type);
Assert.notNull(entity, String.format("Unknown persistent type [%s]", type.getName()));
return entity;
}
/* (non-Javadoc)
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext#contains(java.lang.Class)
*/
@Override
public boolean contains(Class<?> type) {
return entitiesByType.containsKey(type);
}
/**
* @author Jens Schauder
* @since 1.5.1
*/
enum DataTypeProvider {
EntityUserType {
@Override
public DataType getDataType(CassandraPersistentEntity<?> entity) {
return entity.getUserType();
}
},
FrozenLiteral {
@Override
public DataType getDataType(CassandraPersistentEntity<?> entity) {
return new FrozenLiteralDataType(entity.getTableName());
}
};
/**
* Return the data type for the {@link CassandraPersistentEntity}.
*
* @param entity must not be {@literal null}.
* @return
*/
abstract DataType getDataType(CassandraPersistentEntity<?> entity);
}
}