/* * 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 java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.springframework.cassandra.core.Ordering; import org.springframework.cassandra.core.PrimaryKeyType; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryAccessor; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.util.SpelUtils; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; 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.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.expression.spel.support.StandardEvaluationContext; 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.DataType.Name; import com.datastax.driver.core.UserType; /** * Cassandra specific {@link org.springframework.data.mapping.model.AnnotationBasedPersistentProperty} implementation. * * @author Alex Shvid * @author Matthew T. Adams * @author Antoine Toulme * @author Mark Paluch * @author John Blum */ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentProperty<CassandraPersistentProperty> implements CassandraPersistentProperty, ApplicationContextAware { private final UserTypeResolver userTypeResolver; private StandardEvaluationContext spelContext; /** * Whether this property has been explicitly instructed to force quote column names. */ private Boolean forceQuote; /** * An unmodifiable list of this property's column names. */ private List<CqlIdentifier> columnNames; /** * Create a new {@link BasicCassandraPersistentProperty}. * * @param property the actual {@link Property} in the domain entity corresponding to this persistent entity. * @param owner the containing object or {@link CassandraPersistentEntity} of this persistent property. * @param simpleTypeHolder mapping of Java [simple|wrapper] types to Cassandra data types. */ public BasicCassandraPersistentProperty(Property property, CassandraPersistentEntity<?> owner, CassandraSimpleTypeHolder simpleTypeHolder) { this(property, owner, simpleTypeHolder, null); } /** * Create a new {@link BasicCassandraPersistentProperty}. * * @param property the actual {@link Property} in the domain entity corresponding to this persistent entity. * @param owner the containing object or {@link CassandraPersistentEntity} of this persistent property. * @param simpleTypeHolder mapping of Java [simple|wrapper] types to Cassandra data types. * @param userTypeResolver resolver for user-defined types. */ public BasicCassandraPersistentProperty(Property property, CassandraPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder, UserTypeResolver userTypeResolver) { super(property, owner, simpleTypeHolder); this.userTypeResolver = userTypeResolver; if (owner.getApplicationContext() != null) { setApplicationContext(owner.getApplicationContext()); } } /* (non-Javadoc) * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) */ @Override public void setApplicationContext(ApplicationContext context) { Assert.notNull(context, "ApplicationContext must not be null"); spelContext = new StandardEvaluationContext(); spelContext.addPropertyAccessor(new BeanFactoryAccessor()); spelContext.setBeanResolver(new BeanFactoryResolver(context)); spelContext.setRootObject(context); } /* (non-Javadoc) * @see org.springframework.data.mapping.model.AbstractPersistentProperty#getOwner() */ @Override public CassandraPersistentEntity<?> getOwner() { return (CassandraPersistentEntity<?>) super.getOwner(); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#isCompositePrimaryKey() */ @Override public boolean isCompositePrimaryKey() { return (AnnotatedElementUtils.findMergedAnnotation(getType(), PrimaryKeyClass.class) != null); } /** * @return */ public Class<?> getCompositePrimaryKeyType() { return (isCompositePrimaryKey() ? getType() : null); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#getCompositePrimaryKeyTypeInformation() */ @Override public TypeInformation<?> getCompositePrimaryKeyTypeInformation() { return (isCompositePrimaryKey() ? ClassTypeInformation.from(getCompositePrimaryKeyType()) : null); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#getColumnName() */ @Override public CqlIdentifier getColumnName() { List<CqlIdentifier> columnNames = getColumnNames(); Assert.state(columnNames.size() == 1, String.format("Property [%s] has no single column mapping", getName())); return columnNames.get(0); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#getPrimaryKeyOrdering() */ @Override public Optional<Ordering> getPrimaryKeyOrdering() { return findAnnotation(PrimaryKeyColumn.class).map(PrimaryKeyColumn::ordering); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#getDataType() */ /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#getDataType() */ @Override public DataType getDataType() { DataType dataType = findDataType(); if (dataType == null) { throw new InvalidDataAccessApiUsageException(String.format( "Unknown type [%s] for property [%s] in entity [%s]; only primitive types and Collections or Maps of primitive types are allowed", getType(), getName(), getOwner().getName())); } return dataType; } private DataType findDataType() { Optional<CassandraType> cassandraType = findAnnotation(CassandraType.class); if (cassandraType.isPresent()) { return getDataTypeFor(cassandraType.get()); } if (isMap()) { List<TypeInformation<?>> args = getTypeInformation().getTypeArguments(); ensureTypeArguments(args.size(), 2); return DataType.map(getDataTypeFor(args.get(0).getType()), getDataTypeFor(args.get(1).getType())); } if (isCollectionLike()) { List<TypeInformation<?>> args = getTypeInformation().getTypeArguments(); ensureTypeArguments(args.size(), 1); if (Set.class.isAssignableFrom(getType())) { return DataType.set(getDataTypeFor(args.get(0).getType())); } if (List.class.isAssignableFrom(getType())) { return DataType.list(getDataTypeFor(args.get(0).getType())); } } return CassandraSimpleTypeHolder.getDataTypeFor(getType()); } private DataType getDataTypeFor(CassandraType annotation) { DataType.Name type = annotation.type(); switch (type) { case MAP: ensureTypeArguments(annotation.typeArguments().length, 2); return DataType.map(getDataTypeFor(annotation.typeArguments()[0]), getDataTypeFor(annotation.typeArguments()[1])); case LIST: ensureTypeArguments(annotation.typeArguments().length, 1); if (annotation.typeArguments()[0] == Name.UDT) { return DataType.list(getUserType(annotation)); } return DataType.list(getDataTypeFor(annotation.typeArguments()[0])); case SET: ensureTypeArguments(annotation.typeArguments().length, 1); if (annotation.typeArguments()[0] == Name.UDT) { return DataType.set(getUserType(annotation)); } return DataType.set(getDataTypeFor(annotation.typeArguments()[0])); case UDT: return getUserType(annotation); default: return CassandraSimpleTypeHolder.getDataTypeFor(type); } } private DataType getUserType(CassandraType annotation) { if (!StringUtils.hasText(annotation.userTypeName())) { throw new InvalidDataAccessApiUsageException( String.format("Expected user type name in property ['%s'] of type ['%s'] in entity [%s]", getName(), getType(), getOwner().getName())); } CqlIdentifier identifier = CqlIdentifier.cqlId(annotation.userTypeName()); UserType userType = userTypeResolver.resolveType(identifier); if (userType == null) { throw new MappingException(String.format("User type [%s] not found", identifier)); } return userType; } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#isIndexed() */ @Override public boolean isIndexed() { return isAnnotationPresent(Indexed.class); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#isClusterKeyColumn() */ @Override public boolean isClusterKeyColumn() { return findAnnotation(PrimaryKeyColumn.class) .filter(primaryKeyColumn -> PrimaryKeyType.CLUSTERED.equals(primaryKeyColumn.type())).isPresent(); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#isPartitionKeyColumn() */ @Override public boolean isPartitionKeyColumn() { return findAnnotation(PrimaryKeyColumn.class) .filter(primaryKeyColumn -> PrimaryKeyType.PARTITIONED.equals(primaryKeyColumn.type())).isPresent(); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#isPrimaryKeyColumn() */ @Override public boolean isPrimaryKeyColumn() { return isAnnotationPresent(PrimaryKeyColumn.class); } protected DataType getDataTypeFor(DataType.Name dataTypeName) { DataType dataType = CassandraSimpleTypeHolder.getDataTypeFor(dataTypeName); if (dataType == null) { throw new InvalidDataAccessApiUsageException(String.format( "Only primitive types are allowed inside Collections for property [%1$s] of type [%2$s] in entity [%3$s]", getName(), getType(), getOwner().getName())); } return dataType; } protected DataType getDataTypeFor(Class<?> javaType) { Optional<CassandraPersistentEntity<?>> optionalEntity = getOwner().getMappingContext() .getPersistentEntity(javaType); Optional<CassandraPersistentEntity<?>> udtEntity = optionalEntity .filter(CassandraPersistentEntity::isUserDefinedType); if (udtEntity.isPresent()) { return udtEntity.map(CassandraPersistentEntity::getUserType).get(); } DataType dataType = CassandraSimpleTypeHolder.getDataTypeFor(javaType); if (dataType == null) { throw new InvalidDataAccessApiUsageException(String.format( "Only primitive types are allowed inside Collections for property [%1$s] of type ['%2$s'] in entity [%3$s]", getName(), getType(), getOwner().getName())); } return dataType; } protected void ensureTypeArguments(int args, int expected) { if (args != expected) { throw new InvalidDataAccessApiUsageException( String.format("Expected [%1$s] typed arguments for property ['%2$s'] of type ['%3$s'] in entity [%4$s]", expected, getName(), getType(), getOwner().getName())); } } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#getColumnNames() */ @Override public List<CqlIdentifier> getColumnNames() { columnNames = (columnNames != null ? columnNames : Collections.unmodifiableList(determineColumnNames())); return columnNames; } protected List<CqlIdentifier> determineColumnNames() { List<CqlIdentifier> columnNames = new ArrayList<>(); if (isCompositePrimaryKey()) { // then the id type has @PrimaryKeyClass addCompositePrimaryKeyColumnNames(getCompositePrimaryKeyEntity(), columnNames); } else { // else we're dealing with a single-column field String defaultName = getName(); // TODO: replace with naming strategy class String overriddenName; boolean forceQuote; if (isIdProperty()) { // then the id is of a simple type (since it's not a composite primary key) Optional<PrimaryKey> optionalPrimaryKey = findAnnotation(PrimaryKey.class); overriddenName = optionalPrimaryKey.map(PrimaryKey::value).orElse(""); forceQuote = optionalPrimaryKey.map(PrimaryKey::forceQuote).orElse(false); } else if (isPrimaryKeyColumn()) { // then it's a simple type Optional<PrimaryKeyColumn> optionalPrimaryKey = findAnnotation(PrimaryKeyColumn.class); overriddenName = optionalPrimaryKey.map(PrimaryKeyColumn::value).orElse(""); forceQuote = optionalPrimaryKey.map(PrimaryKeyColumn::forceQuote).orElse(false); } else { // then it's a vanilla column with the assumption that it's mapped to a single column Optional<Column> optionalColumn = findAnnotation(Column.class); overriddenName = optionalColumn.map(Column::value).orElse(""); forceQuote = optionalColumn.map(Column::forceQuote).orElse(false); } columnNames.add(createColumnName(defaultName, overriddenName, forceQuote)); } return columnNames; } protected CqlIdentifier createColumnName(String defaultName, String overriddenName, boolean forceQuote) { String name = defaultName; if (StringUtils.hasText(overriddenName)) { name = (spelContext != null ? SpelUtils.evaluate(overriddenName, spelContext) : overriddenName); } return cqlId(name, forceQuote); } protected void addCompositePrimaryKeyColumnNames(CassandraPersistentEntity<?> compositePrimaryKeyEntity, final List<CqlIdentifier> columnNames) { compositePrimaryKeyEntity.getPersistentProperties().forEach(property -> { if (property.isCompositePrimaryKey()) { addCompositePrimaryKeyColumnNames(property.getCompositePrimaryKeyEntity(), columnNames); } else { columnNames.add(property.getColumnName()); } }); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#setColumnName(org.springframework.cassandra.core.cql.CqlIdentifier) */ @Override public void setColumnName(CqlIdentifier columnName) { Assert.notNull(columnName, "columnName must not be null"); setColumnNames(Collections.singletonList(columnName)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#setColumnNames(java.util.List) */ @Override public void setColumnNames(List<CqlIdentifier> columnNames) { Assert.notNull(columnNames, "List of column names must not be null"); // force calculation of columnNames if not known yet getColumnNames(); Assert.state(this.columnNames.size() == columnNames.size(), String.format( "Property [%s] of entity [%s] is mapped to [%s] column%s, but given column name list has size [%s]", getName(), getOwner().getType().getName(), this.columnNames.size(), this.columnNames.size() == 1 ? "" : "s", columnNames.size())); this.columnNames = Collections.unmodifiableList(new ArrayList<>(columnNames)); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#setForceQuote(boolean) */ @Override public void setForceQuote(boolean forceQuote) { if (this.forceQuote != null && this.forceQuote == forceQuote) { return; } else { this.forceQuote = forceQuote; } List<CqlIdentifier> columnNames = getColumnNames() // .stream() // .map(CqlIdentifier::getUnquoted) // .map(name -> cqlId(name, forceQuote)) // .collect(Collectors.toList()); setColumnNames(columnNames); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#getCompositePrimaryKeyProperties() */ @Override public List<CassandraPersistentProperty> getCompositePrimaryKeyProperties() { Assert.state(isCompositePrimaryKey(), String.format("[%s] does not represent a composite primary key property", getName())); return getCompositePrimaryKeyEntity().getCompositePrimaryKeyProperties(); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#getCompositePrimaryKeyEntity() */ @Override public CassandraPersistentEntity<?> getCompositePrimaryKeyEntity() { CassandraMappingContext mappingContext = getOwner().getMappingContext(); Assert.state(mappingContext != null, "CassandraMappingContext needed"); return mappingContext.getRequiredPersistentEntity(getCompositePrimaryKeyTypeInformation()); } /* (non-Javadoc) * @see org.springframework.data.mapping.model.AbstractPersistentProperty#getAssociation() */ @Override public Optional<Association<CassandraPersistentProperty>> getAssociation() { return Optional.empty(); } /* (non-Javadoc) * @see org.springframework.data.mapping.model.AbstractPersistentProperty#createAssociation() */ @Override protected Association<CassandraPersistentProperty> createAssociation() { return new Association<>(this, null); } /* (non-Javadoc) * @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty#isMapLike() */ @Override public boolean isMapLike() { return ClassUtils.isAssignable(Map.class, getType()); } }