/*
* 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.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.cassandra.core.QueryOptionsUtil;
import org.springframework.cassandra.core.WriteOptions;
import org.springframework.cassandra.core.cql.CqlIdentifier;
import org.springframework.data.cassandra.convert.QueryMapper;
import org.springframework.data.cassandra.convert.UpdateMapper;
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.CriteriaDefinition;
import org.springframework.data.cassandra.core.query.CriteriaDefinition.Predicate;
import org.springframework.data.cassandra.core.query.Filter;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.cassandra.core.query.Update;
import org.springframework.data.cassandra.core.query.Update.AddToMapOp;
import org.springframework.data.cassandra.core.query.Update.AddToOp;
import org.springframework.data.cassandra.core.query.Update.AddToOp.Mode;
import org.springframework.data.cassandra.core.query.Update.AssignmentOp;
import org.springframework.data.cassandra.core.query.Update.IncrOp;
import org.springframework.data.cassandra.core.query.Update.RemoveOp;
import org.springframework.data.cassandra.core.query.Update.SetAtIndexOp;
import org.springframework.data.cassandra.core.query.Update.SetAtKeyOp;
import org.springframework.data.cassandra.core.query.Update.SetOp;
import org.springframework.data.cassandra.mapping.CassandraPersistentEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.util.Assert;
import com.datastax.driver.core.RegularStatement;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.Assignment;
import com.datastax.driver.core.querybuilder.Clause;
import com.datastax.driver.core.querybuilder.Delete;
import com.datastax.driver.core.querybuilder.Ordering;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.datastax.driver.core.querybuilder.Select.Selection;
import com.datastax.driver.core.querybuilder.Select.SelectionOrAlias;
import com.google.common.primitives.Ints;
/**
* Statement factory to render {@link Statement} from {@link Query} and {@link Update} objects.
*
* @author Mark Paluch
* @author John Blum
* @see org.springframework.data.cassandra.core.query.Query
* @see org.springframework.data.cassandra.core.query.Update
* @see com.datastax.driver.core.querybuilder.Assignment
* @see com.datastax.driver.core.querybuilder.Clause
* @see com.datastax.driver.core.querybuilder.Delete
* @see com.datastax.driver.core.querybuilder.Ordering
* @see com.datastax.driver.core.querybuilder.QueryBuilder
* @see com.datastax.driver.core.querybuilder.Select
* @since 2.0
*/
public class StatementFactory {
private final QueryMapper queryMapper;
private final UpdateMapper updateMapper;
/**
* Create {@link StatementFactory} given {@link UpdateMapper}.
*
* @param updateMapper must not be {@literal null}.
*/
public StatementFactory(UpdateMapper updateMapper) {
this(updateMapper, updateMapper);
}
/**
* Create {@link StatementFactory} given {@link QueryMapper} and {@link UpdateMapper}.
*
* @param queryMapper must not be {@literal null}.
* @param updateMapper must not be {@literal null}.
*/
public StatementFactory(QueryMapper queryMapper, UpdateMapper updateMapper) {
Assert.notNull(queryMapper, "QueryMapper must not be null");
Assert.notNull(updateMapper, "UpdateMapper must not be null");
this.queryMapper = queryMapper;
this.updateMapper = updateMapper;
}
/**
* Returns the {@link QueryMapper} used to map {@link Query} to CQL-specific data types.
*
* @return the {@link QueryMapper} used to map {@link Query} to CQL-specific data types.
* @see org.springframework.data.cassandra.convert.QueryMapper
*/
protected QueryMapper getQueryMapper() {
return this.queryMapper;
}
/**
* Returns the {@link UpdateMapper} used to map {@link Update} to CQL-specific data types.
*
* @return the {@link UpdateMapper} used to map {@link Update} to CQL-specific data types.
* @see org.springframework.data.cassandra.convert.UpdateMapper
*/
protected UpdateMapper getUpdateMapper() {
return this.updateMapper;
}
/**
* Create a {@literal SELECT} statement by mapping {@link Query} to {@link Select}.
*
* @param query must not be {@literal null}.
* @param entity must not be {@literal null}.
* @return the rendered {@link RegularStatement}.
*/
public RegularStatement select(Query query, CassandraPersistentEntity<?> entity) {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entity, "Entity must not be null");
Filter filter = getQueryMapper().getMappedObject(query, entity);
List<Selector> selectors = getQueryMapper().getMappedSelectors(query.getColumns(), entity);
Sort sort = Optional.ofNullable(query.getSort())
.map(querySort -> getQueryMapper().getMappedSort(querySort, entity))
.orElse(null);
Select select = select(selectors, entity.getTableName(), filter, sort);
query.getQueryOptions().ifPresent(queryOptions -> QueryOptionsUtil.addQueryOptions(select, queryOptions));
if (query.getLimit() > 0) {
select.limit(Ints.checkedCast(query.getLimit()));
}
if (query.isAllowFiltering()) {
select.allowFiltering();
}
query.getPagingState().ifPresent(select::setPagingState);
return select;
}
private static Select select(List<Selector> selectors, CqlIdentifier from, Filter filter, Sort sort) {
Select select;
if (selectors.isEmpty()) {
select = QueryBuilder.select().all().from(from.toCql());
} else {
Selection selection = QueryBuilder.select();
selectors.forEach(selector -> {
selector.getAlias().map(CqlIdentifier::toCql).ifPresent(getSelection(selection, selector)::as);
});
select = selection.from(from.toCql());
}
for (CriteriaDefinition criteriaDefinition : filter) {
select.where(toClause(criteriaDefinition));
}
if (sort != null) {
List<Ordering> orderings = new ArrayList<>();
for (Order order : sort) {
if (order.isAscending()) {
orderings.add(QueryBuilder.asc(order.getProperty()));
} else {
orderings.add(QueryBuilder.desc(order.getProperty()));
}
}
if (!orderings.isEmpty()) {
select.orderBy(orderings.toArray(new Ordering[orderings.size()]));
}
}
return select;
}
private static SelectionOrAlias getSelection(Selection selection, Selector selector) {
if (selector instanceof FunctionCall) {
Object[] objects = ((FunctionCall) selector).getParameters().stream().map(param -> {
if (param instanceof ColumnSelector) {
return QueryBuilder.column(((ColumnSelector) param).getExpression());
}
return param;
}).toArray();
return selection.fcall(selector.getExpression(), objects);
}
return selection.column(selector.getExpression());
}
/**
* Create an {@literal UPDATE} statement by mapping {@link Query} to {@link Update}.
*
* @param query must not be {@literal null}.
* @param entity must not be {@literal null}.
* @return the rendered {@link RegularStatement}.
*/
public RegularStatement update(Query query, Update updateObj, CassandraPersistentEntity<?> entity) {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entity, "Entity must not be null");
Filter filter = getQueryMapper().getMappedObject(query, entity);
Update mappedUpdate = getUpdateMapper().getMappedObject(updateObj, entity);
com.datastax.driver.core.querybuilder.Update update = update(entity.getTableName(), mappedUpdate, filter);
query.getQueryOptions().ifPresent(queryOptions -> {
if (queryOptions instanceof WriteOptions) {
QueryOptionsUtil.addWriteOptions(update, (WriteOptions) queryOptions);
} else {
QueryOptionsUtil.addQueryOptions(update, queryOptions);
}
});
query.getPagingState().ifPresent(update::setPagingState);
return update;
}
private static com.datastax.driver.core.querybuilder.Update update(CqlIdentifier table, Update mappedUpdate,
Filter filter) {
com.datastax.driver.core.querybuilder.Update update = QueryBuilder.update(table.toCql());
for (AssignmentOp assignmentOp : mappedUpdate.getUpdateOperations()) {
update.with(getAssignment(assignmentOp));
}
for (CriteriaDefinition criteriaDefinition : filter) {
update.where(toClause(criteriaDefinition));
}
return update;
}
private static Assignment getAssignment(AssignmentOp assignmentOp) {
if (assignmentOp instanceof SetOp) {
return getAssignment((SetOp) assignmentOp);
}
if (assignmentOp instanceof RemoveOp) {
return getAssignment((RemoveOp) assignmentOp);
}
if (assignmentOp instanceof IncrOp) {
return getAssignment((IncrOp) assignmentOp);
}
if (assignmentOp instanceof AddToOp) {
return getAssignment((AddToOp) assignmentOp);
}
if (assignmentOp instanceof AddToMapOp) {
return getAssignment((AddToMapOp) assignmentOp);
}
throw new IllegalArgumentException(String.format("UpdateOp %s not supported", assignmentOp));
}
private static Assignment getAssignment(IncrOp incrOp) {
return incrOp.getValue().intValue() > 0
? QueryBuilder.incr(incrOp.getColumnName().toCql(), Math.abs(incrOp.getValue().intValue()))
: QueryBuilder.decr(incrOp.getColumnName().toCql(), Math.abs(incrOp.getValue().intValue()));
}
private static Assignment getAssignment(SetOp updateOp) {
if (updateOp instanceof SetAtIndexOp) {
SetAtIndexOp op = (SetAtIndexOp) updateOp;
return QueryBuilder.setIdx(op.getColumnName().toCql(), op.getIndex(), op.getValue());
}
if (updateOp instanceof SetAtKeyOp) {
SetAtKeyOp op = (SetAtKeyOp) updateOp;
return QueryBuilder.put(op.getColumnName().toCql(), op.getKey(), op.getValue());
}
return QueryBuilder.set(updateOp.getColumnName().toCql(), updateOp.getValue());
}
private static Assignment getAssignment(RemoveOp updateOp) {
if (updateOp.getValue() instanceof Set) {
return QueryBuilder.removeAll(updateOp.getColumnName().toCql(), (Set) updateOp.getValue());
}
if (updateOp.getValue() instanceof List) {
return QueryBuilder.discardAll(updateOp.getColumnName().toCql(), (List) updateOp.getValue());
}
return QueryBuilder.remove(updateOp.getColumnName().toCql(), updateOp.getValue());
}
@SuppressWarnings("unchecked")
private static Assignment getAssignment(AddToOp updateOp) {
if (updateOp.getValue() instanceof Set) {
return QueryBuilder.addAll(updateOp.getColumnName().toCql(), (Set) updateOp.getValue());
}
return Mode.PREPEND.equals(updateOp.getMode())
? QueryBuilder.prependAll(updateOp.getColumnName().toCql(), (List) updateOp.getValue())
: QueryBuilder.appendAll(updateOp.getColumnName().toCql(), (List) updateOp.getValue());
}
private static Assignment getAssignment(AddToMapOp updateOp) {
return QueryBuilder.putAll(updateOp.getColumnName().toCql(), updateOp.getValue());
}
/**
* Create a {@literal DELETE} statement by mapping {@link Query} to {@link Delete}.
*
* @param query must not be {@literal null}.
* @param entity must not be {@literal null}.
* @return the rendered {@link RegularStatement}.
*/
public RegularStatement delete(Query query, CassandraPersistentEntity<?> entity) {
Assert.notNull(query, "Query must not be null");
Assert.notNull(entity, "Entity must not be null");
Filter filter = getQueryMapper().getMappedObject(query, entity);
List<String> columnNames = getQueryMapper().getMappedColumnNames(query.getColumns(), entity);
Delete delete = delete(columnNames, entity.getTableName(), filter);
query.getQueryOptions().ifPresent(queryOptions -> QueryOptionsUtil.addQueryOptions(delete, queryOptions));
query.getPagingState().ifPresent(delete::setPagingState);
return delete;
}
private static Delete delete(List<String> columnNames, CqlIdentifier from, Filter filter) {
Delete select;
if (columnNames.isEmpty()) {
select = QueryBuilder.delete().all().from(from.toCql());
} else {
Delete.Selection selection = QueryBuilder.delete();
columnNames.forEach(selection::column);
select = selection.from(from.toCql());
}
for (CriteriaDefinition criteriaDefinition : filter) {
select.where(toClause(criteriaDefinition));
}
return select;
}
private static Clause toClause(CriteriaDefinition criteriaDefinition) {
Predicate predicate = criteriaDefinition.getPredicate();
String columnName = criteriaDefinition.getColumnName().toCql();
switch (predicate.getOperator().toString()) {
case "=":
return QueryBuilder.eq(columnName, predicate.getValue());
case ">":
return QueryBuilder.gt(columnName, predicate.getValue());
case ">=":
return QueryBuilder.gte(columnName, predicate.getValue());
case "<":
return QueryBuilder.lt(columnName, predicate.getValue());
case "<=":
return QueryBuilder.lte(columnName, predicate.getValue());
case "IN":
if (predicate.getValue() instanceof List) {
return QueryBuilder.in(columnName, (List<?>) predicate.getValue());
}
if (predicate.getValue().getClass().isArray()) {
return QueryBuilder.in(columnName, (Object[]) predicate.getValue());
}
return QueryBuilder.in(columnName, predicate.getValue());
case "LIKE":
return QueryBuilder.like(columnName, predicate.getValue());
case "CONTAINS":
return QueryBuilder.contains(columnName, predicate.getValue());
case "CONTAINS KEY":
return QueryBuilder.containsKey(columnName, predicate.getValue());
}
throw new IllegalArgumentException(String.format("Criteria %s %s %s not supported",
columnName, predicate.getOperator(), predicate.getValue()));
}
}