/*
* Copyright 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 java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.cassandra.core.cql.CqlIdentifier;
import org.springframework.data.cassandra.core.query.ColumnName;
import org.springframework.data.cassandra.core.query.Columns;
import org.springframework.data.cassandra.core.query.Columns.ColumnSelector;
import org.springframework.data.cassandra.core.query.Columns.FunctionCall;
import org.springframework.data.cassandra.core.query.Columns.Selector;
import org.springframework.data.cassandra.core.query.Criteria;
import org.springframework.data.cassandra.core.query.CriteriaDefinition;
import org.springframework.data.cassandra.core.query.CriteriaDefinition.Predicate;
import org.springframework.data.cassandra.core.query.Filter;
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.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
/**
* Map {@link org.springframework.data.cassandra.core.query.Query} to CQL-specific data types.
*
* @author Mark Paluch
* @see org.springframework.data.cassandra.core.query.ColumnName
* @see org.springframework.data.cassandra.core.query.Columns
* @see org.springframework.data.cassandra.core.query.Criteria
* @see org.springframework.data.cassandra.core.query.Filter
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext
* @see org.springframework.data.cassandra.mapping.CassandraPersistentEntity
* @see org.springframework.data.cassandra.mapping.CassandraPersistentProperty
* @see org.springframework.data.domain.Sort
* @see org.springframework.data.mapping.PersistentProperty
* @see org.springframework.data.mapping.PropertyPath
* @see org.springframework.data.mapping.context.MappingContext
* @see org.springframework.data.mapping.context.PersistentPropertyPath
* @see org.springframework.data.util.TypeInformation
* @since 2.0
*/
public class QueryMapper {
private final CassandraConverter converter;
private final CassandraMappingContext mappingContext;
/**
* Creates a new {@link QueryMapper} with the given {@link CassandraConverter}.
*
* @param converter must not be {@literal null}.
*/
public QueryMapper(CassandraConverter converter) {
Assert.notNull(converter, "CassandraConverter must not be null");
this.converter = converter;
this.mappingContext = converter.getMappingContext();
}
/**
* Returns the configured {@link CassandraConverter} used to convert object values into
* Cassandra column typed values.
*
* @return the configured {@link CassandraConverter}.
* @see org.springframework.data.cassandra.convert.CassandraConverter
*/
protected CassandraConverter getConverter() {
return this.converter;
}
/**
* Returns the configured {@link CassandraMappingContext} containing mapping meta-data (persistent entities
* and properties) used to store (map) objects to Cassandra tables (rows/columns).
*
* @return the configured {@link CassandraMappingContext}.
* @see org.springframework.data.cassandra.mapping.CassandraMappingContext
*/
protected CassandraMappingContext getMappingContext() {
return this.mappingContext;
}
/**
* Return {@link ColumnSelector}s for all columns of {@link CassandraPersistentEntity}.
*
* @param entity must not be {@literal null}.
* @return {@link ColumnSelector}s for all columns of {@link CassandraPersistentEntity}.
*/
public List<Selector> getColumns(CassandraPersistentEntity<?> entity) {
return entity.getPersistentProperties() //
.flatMap(p -> p.getColumnNames().stream()).map(ColumnSelector::from) //
.collect(Collectors.toList());
}
/**
* Map a {@link Filter} with a {@link CassandraPersistentEntity type hint}. Filter mapping translates property names
* to column names and maps {@link Predicate} values to simple Cassandra values.
*
* @param filter must not be {@literal null}.
* @param entity must not be {@literal null}.
* @return the mapped {@link Filter}.
*/
public Filter getMappedObject(Filter filter, CassandraPersistentEntity<?> entity) {
Assert.notNull(filter, "Filter must not be null");
Assert.notNull(entity, "Entity must not be null");
List<CriteriaDefinition> result = new ArrayList<>();
for (CriteriaDefinition criteriaDefinition : filter) {
Field field = createPropertyField(entity, criteriaDefinition.getColumnName());
Predicate predicate = criteriaDefinition.getPredicate();
Optional<Object> value = Optional.ofNullable(predicate.getValue());
TypeInformation<?> typeInformation = getTypeInformation(field, value);
Optional<Object> mappedValue = getConverter().convertToColumnType(value, typeInformation);
Predicate mappedPredicate = new Predicate(predicate.getOperator(), mappedValue.orElse(null));
result.add(Criteria.of(field.getMappedKey(), mappedPredicate));
}
return Filter.from(result);
}
/**
* Map {@link Columns} with a {@link CassandraPersistentEntity type hint} to {@link ColumnSelector}s.
*
* @param columns must not be {@literal null}.
* @param entity must not be {@literal null}.
* @return the mapped {@link Selector}s.
*/
public List<Selector> getMappedSelectors(Columns columns, CassandraPersistentEntity<?> entity) {
Assert.notNull(columns, "Columns must not be null");
Assert.notNull(entity, "CassandraPersistentEntity must not be null");
if (columns.isEmpty()) {
return Collections.emptyList();
}
List<Selector> selectors = new ArrayList<>();
for (ColumnName column : columns) {
Field field = createPropertyField(entity, column);
columns.getSelector(column).ifPresent(selector -> {
getCqlIdentifier(column, field).ifPresent(cqlIdentifier -> {
selectors.add(getMappedSelector(selector, cqlIdentifier));
});
});
}
if (columns.isEmpty()) {
entity.doWithProperties((PropertyHandler<CassandraPersistentProperty>) property -> {
if (property.isCompositePrimaryKey()) {
for (CqlIdentifier cqlIdentifier : property.getColumnNames()) {
selectors.add(ColumnSelector.from(cqlIdentifier.toCql()));
}
} else {
selectors.add(ColumnSelector.from(property.getColumnName().toCql()));
}
});
}
return selectors;
}
private Selector getMappedSelector(Selector selector, CqlIdentifier cqlIdentifier) {
if (selector instanceof ColumnSelector) {
ColumnSelector columnSelector = (ColumnSelector) selector;
ColumnSelector mappedColumnSelector = ColumnSelector.from(cqlIdentifier);
return columnSelector.getAlias()
.map(mappedColumnSelector::as)
.orElse(mappedColumnSelector);
}
if (selector instanceof FunctionCall) {
FunctionCall functionCall = (FunctionCall) selector;
List<Object> mappedParameters = functionCall.getParameters()
.stream()
.map(obj -> {
if (obj instanceof Selector) {
return getMappedSelector((Selector) obj, cqlIdentifier);
}
return obj;
}) //
.collect(Collectors.toList());
FunctionCall mappedFunctionCall =
FunctionCall.from(functionCall.getExpression(), mappedParameters.toArray());
return functionCall.getAlias() //
.map(mappedFunctionCall::as) //
.orElse(mappedFunctionCall);
}
throw new IllegalArgumentException(String.format("Selector [%s] not supported", selector));
}
/**
* Map {@link Columns} with a {@link CassandraPersistentEntity type hint} to column names for included columns.
* Function call selectors or other {@link org.springframework.data.cassandra.core.query.Columns.Selector} types are
* not included.
*
* @param columns must not be {@literal null}.
* @param entity must not be {@literal null}.
* @return the mapped column names.
*/
public List<String> getMappedColumnNames(Columns columns, CassandraPersistentEntity<?> entity) {
Assert.notNull(columns, "Columns must not be null");
Assert.notNull(entity, "CassandraPersistentEntity must not be null");
if (columns.isEmpty()) {
return Collections.emptyList();
}
List<String> columnNames = new ArrayList<>();
Set<PersistentProperty<?>> seen = new HashSet<>();
for (ColumnName column : columns) {
Field field = createPropertyField(entity, column);
field.getProperty().ifPresent(seen::add);
columns.getSelector(column) //
.filter(selector -> selector instanceof ColumnSelector) //
.ifPresent(columnSelector -> {
getCqlIdentifier(column, field).map(CqlIdentifier::toCql).ifPresent(columnNames::add);
});
}
if (columns.isEmpty()) {
entity.doWithProperties((PropertyHandler<CassandraPersistentProperty>) property -> {
if (property.isCompositePrimaryKey()) {
return;
}
if (seen.add(property)) {
columnNames.add(property.getColumnName().toCql());
}
});
}
return columnNames;
}
public Sort getMappedSort(Sort sort, CassandraPersistentEntity<?> entity) {
Assert.notNull(sort, "Sort must not be null");
Assert.notNull(entity, "CassandraPersistentEntity must not be null");
if (!sort.iterator().hasNext()) {
return sort;
}
List<Order> mappedOrders = new ArrayList<>();
for (Order order : sort) {
ColumnName columnName = ColumnName.from(order.getProperty());
Field field = createPropertyField(entity, columnName);
Order mappedOrder = getCqlIdentifier(columnName, field)
.map(cqlIdentifier -> new Order(order.getDirection(), cqlIdentifier.toCql())).orElse(order);
mappedOrders.add(mappedOrder);
}
return Sort.by(mappedOrders);
}
private Optional<CqlIdentifier> getCqlIdentifier(ColumnName column, Field field) {
try {
if (field.getProperty().isPresent()) {
return field.getProperty().map(CassandraPersistentProperty::getColumnName);
}
if (column.getColumnName().isPresent()) {
return column.getColumnName().map(CqlIdentifier::cqlId);
}
return column.getCqlIdentifier();
} catch (IllegalStateException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
/**
* @param entity
* @param key
* @return
*/
protected Field createPropertyField(CassandraPersistentEntity<?> entity, ColumnName key) {
return Optional.ofNullable(entity).<Field>map(e -> new MetadataBackedField(key, e, getMappingContext()))
.orElseGet(() -> new Field(key));
}
@SuppressWarnings("unchecked")
TypeInformation<?> getTypeInformation(Field field, Optional<? extends Object> value) {
return field.getProperty().map(CassandraPersistentProperty::getTypeInformation)
.orElseGet(() ->
value.map(Object::getClass)
.map(ClassTypeInformation::from)
.orElse((ClassTypeInformation) ClassTypeInformation.OBJECT)
);
}
/**
* Value object to represent a field and its meta-information.
*
* @author Mark Paluch
*/
protected static class Field {
protected final ColumnName name;
/**
* Creates a new {@link Field} without meta-information but the given name.
*
* @param name must not be {@literal null} or empty.
*/
public Field(ColumnName name) {
Assert.notNull(name, "Name must not be null!");
this.name = name;
}
/**
* Returns a new {@link Field} with the given name.
*
* @param name must not be {@literal null} or empty.
* @return a new {@link Field} with the given name.
*/
public Field with(ColumnName name) {
return new Field(name);
}
/**
* Returns the underlying {@link CassandraPersistentProperty} backing the field. For path traversals this will be
* the property that represents the value to handle. This means it'll be the leaf property for plain paths or the
* association property in case we refer to an association somewhere in the path.
*
* @return
*/
public Optional<CassandraPersistentProperty> getProperty() {
return Optional.empty();
}
/**
* Returns the key to be used in the mapped document eventually.
*
* @return
*/
public ColumnName getMappedKey() {
return name;
}
}
/**
* Extension of {@link Field} to be backed with mapping metadata.
*
* @author Mark Paluch
*/
protected static class MetadataBackedField extends Field {
private final CassandraPersistentEntity<?> entity;
private final MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext;
private final Optional<PersistentPropertyPath<CassandraPersistentProperty>> path;
private final CassandraPersistentProperty property;
private final Optional<CassandraPersistentProperty> optionalProperty;
/**
* Creates a new {@link MetadataBackedField} with the given name, {@link CassandraPersistentEntity}
* and {@link MappingContext}.
*
* @param name must not be {@literal null} or empty.
* @param entity must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
*/
public MetadataBackedField(ColumnName name, CassandraPersistentEntity<?> entity,
MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext) {
this(name, entity, mappingContext, null);
}
/**
* Creates a new {@link MetadataBackedField} with the given name, {@link CassandraPersistentProperty} and
* {@link MappingContext} with the given {@link CassandraPersistentProperty}.
*
* @param name must not be {@literal null} or empty.
* @param entity must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
* @param property may be {@literal null}.
*/
public MetadataBackedField(ColumnName name, CassandraPersistentEntity<?> entity,
MappingContext<? extends CassandraPersistentEntity<?>, CassandraPersistentProperty> mappingContext,
CassandraPersistentProperty property) {
super(name);
Assert.notNull(entity, "CassandraPersistentEntity must not be null");
this.entity = entity;
this.mappingContext = mappingContext;
this.path = getPath(name.toCql());
this.property = path.map(PersistentPropertyPath::getLeafProperty).orElse(property);
this.optionalProperty = Optional.ofNullable(this.property);
}
/**
* Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}.
*
* @param pathExpression
* @return
*/
private Optional<PersistentPropertyPath<CassandraPersistentProperty>> getPath(String pathExpression) {
try {
PropertyPath propertyPath = PropertyPath.from(pathExpression.replaceAll("\\.\\d", ""),
entity.getTypeInformation());
PersistentPropertyPath<CassandraPersistentProperty> persistentPropertyPath =
mappingContext.getPersistentPropertyPath(propertyPath);
return Optional.of(persistentPropertyPath);
} catch (PropertyReferenceException e) {
return Optional.empty();
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#with(java.lang.String)
*/
@Override
public MetadataBackedField with(ColumnName name) {
return new MetadataBackedField(name, entity, mappingContext, property);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getProperty()
*/
@Override
public Optional<CassandraPersistentProperty> getProperty() {
return optionalProperty;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getTargetKey()
*/
@Override
public ColumnName getMappedKey() {
return path.map(PersistentPropertyPath::getLeafProperty) //
.map(CassandraPersistentProperty::getColumnName) //
.map(ColumnName::from) //
.orElse(name);
}
}
}