/* * Copyright 2016-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.List; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import java.util.function.Function; import org.springframework.cassandra.core.AsyncCqlOperations; import org.springframework.cassandra.core.AsyncCqlTemplate; import org.springframework.cassandra.core.AsyncSessionCallback; import org.springframework.cassandra.core.CqlProvider; import org.springframework.cassandra.core.GuavaListenableFutureAdapter; import org.springframework.cassandra.core.QueryOptions; import org.springframework.cassandra.core.WriteOptions; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.session.DefaultSessionFactory; import org.springframework.cassandra.core.session.SessionFactory; import org.springframework.cassandra.core.support.CQLExceptionTranslator; import org.springframework.dao.DataAccessException; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.convert.QueryMapper; import org.springframework.data.cassandra.convert.UpdateMapper; import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.concurrent.ListenableFuture; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Session; import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.Statement; import com.datastax.driver.core.exceptions.DriverException; 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.Truncate; import com.datastax.driver.core.querybuilder.Update; /** * Primary implementation of {@link AsyncCassandraOperations}. It simplifies the use of asynchronous Cassandra usage and * helps to avoid common errors. It executes core Cassandra workflow. This class executes CQL queries or updates, * initiating iteration over {@link ResultSet} and catching Cassandra exceptions and translating them to the generic, * more informative exception hierarchy defined in the {@code org.springframework.dao} package. * <p> * Can be used within a service implementation via direct instantiation with a {@link Session} reference, or get * prepared in an application context and given to services as bean reference. * <p> * Note: The {@link Session} should always be configured as a bean in the application context, in the first case given * to the service directly, in the second case to the prepared template. * * @author Mark Paluch * @author John Blum * @see org.springframework.cassandra.core.AsyncCqlOperations * @see org.springframework.data.cassandra.core.AsyncCassandraOperations * @see com.datastax.driver.core.querybuilder.Delete * @see com.datastax.driver.core.querybuilder.Insert * @see com.datastax.driver.core.querybuilder.QueryBuilder * @see com.datastax.driver.core.querybuilder.Select * @see com.datastax.driver.core.querybuilder.Truncate * @see com.datastax.driver.core.querybuilder.Update * @since 2.0 */ public class AsyncCassandraTemplate implements AsyncCassandraOperations { private final AsyncCqlOperations cqlOperations; private final CassandraConverter converter; private final CassandraMappingContext mappingContext; private final CQLExceptionTranslator exceptionTranslator; private final StatementFactory statementFactory; /** * Creates an instance of {@link AsyncCassandraTemplate} initialized with the given {@link Session} and a default * {@link MappingCassandraConverter}. * * @param session {@link Session} used to interact with Cassandra; must not be {@literal null}. * @see CassandraConverter * @see Session */ public AsyncCassandraTemplate(Session session) { this(session, newConverter()); } /** * Creates an instance of {@link AsyncCassandraTemplate} initialized with the given {@link Session} and * {@link CassandraConverter}. * * @param session {@link Session} used to interact with Cassandra; must not be {@literal null}. * @param converter {@link CassandraConverter} used to convert between Java and Cassandra types; must not be * {@literal null}. * @see CassandraConverter * @see Session */ public AsyncCassandraTemplate(Session session, CassandraConverter converter) { this(new DefaultSessionFactory(session), converter); } /** * Creates an instance of {@link AsyncCassandraTemplate} initialized with the given {@link SessionFactory} and * {@link CassandraConverter}. * * @param sessionFactory {@link SessionFactory} used to interact with Cassandra; must not be {@literal null}. * @param converter {@link CassandraConverter} used to convert between Java and Cassandra types; must not be * {@literal null}. * @see CassandraConverter * @see Session */ public AsyncCassandraTemplate(SessionFactory sessionFactory, CassandraConverter converter) { this(new AsyncCqlTemplate(sessionFactory), converter); } /** * Creates an instance of {@link AsyncCassandraTemplate} initialized with the given {@link AsyncCqlTemplate} and * {@link CassandraConverter}. * * @param asyncCqlTemplate {@link AsyncCqlTemplate} used to interact with Cassandra; must not be {@literal null}. * @param converter {@link CassandraConverter} used to convert between Java and Cassandra types; must not be * {@literal null}. * @see CassandraConverter * @see Session */ public AsyncCassandraTemplate(AsyncCqlTemplate asyncCqlTemplate, CassandraConverter converter) { Assert.notNull(asyncCqlTemplate, "AsyncCqlTemplate must not be null"); Assert.notNull(converter, "CassandraConverter must not be null"); this.converter = converter; this.mappingContext = converter.getMappingContext(); this.cqlOperations = asyncCqlTemplate; this.exceptionTranslator = asyncCqlTemplate.getExceptionTranslator(); this.statementFactory = new StatementFactory(new QueryMapper(converter), new UpdateMapper(converter)); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#getAsyncCqlOperations() */ @Override public AsyncCqlOperations getAsyncCqlOperations() { return this.cqlOperations; } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#getConverter() */ @Override public CassandraConverter getConverter() { return this.converter; } /* (non-Javadoc) */ private static MappingCassandraConverter newConverter() { MappingCassandraConverter converter = new MappingCassandraConverter(); converter.afterPropertiesSet(); return converter; } /** * Returns the {@link CassandraMappingContext} used by this template to access mapping meta-data used to * store (map) objects to Cassandra tables. * * @return the {@link CassandraMappingContext} used by this template. * @see org.springframework.data.cassandra.mapping.CassandraMappingContext */ protected CassandraMappingContext getMappingContext() { return this.mappingContext; } /** * Returns the {@link StatementFactory} used by this template to construct and run Cassandra CQL statements. * * @return the {@link StatementFactory} used by this template to construct and run Cassandra CQL statements. * @see org.springframework.data.cassandra.core.StatementFactory */ protected StatementFactory getStatementFactory() { return this.statementFactory; } /* (non-Javadoc) */ private CqlIdentifier getTableName(Object entity) { return getMappingContext().getRequiredPersistentEntity(ClassUtils.getUserClass(entity)).getTableName(); } // ------------------------------------------------------------------------- // Methods dealing with static CQL // ------------------------------------------------------------------------- /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(java.lang.String, java.lang.Class) */ @Override public <T> ListenableFuture<List<T>> select(String cql, Class<T> entityClass) { Assert.hasText(cql, "Statement must not be empty"); return select(new SimpleStatement(cql), entityClass); } @Override public <T> ListenableFuture<Void> select(String cql, Consumer<T> entityConsumer, Class<T> entityClass) throws DataAccessException { Assert.hasText(cql, "Statement must not be empty"); Assert.notNull(entityConsumer, "Entity Consumer must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); return select(new SimpleStatement(cql), entityConsumer, entityClass); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOne(java.lang.String, java.lang.Class) */ @Override public <T> ListenableFuture<T> selectOne(String cql, Class<T> entityClass) { Assert.hasText(cql, "Statement must not be empty"); Assert.notNull(entityClass, "Entity type must not be null"); return selectOne(new SimpleStatement(cql), entityClass); } // ------------------------------------------------------------------------- // Methods dealing with com.datastax.driver.core.Statement // ------------------------------------------------------------------------- /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(com.datastax.driver.core.Statement, java.lang.Class) */ @Override public <T> ListenableFuture<List<T>> select(Statement statement, Class<T> entityClass) { Assert.notNull(statement, "Statement must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); return getAsyncCqlOperations().query(statement, (row, rowNum) -> getConverter().read(entityClass, row)); } @Override public <T> ListenableFuture<Void> select(Statement statement, Consumer<T> entityConsumer, Class<T> entityClass) throws DataAccessException { Assert.notNull(statement, "Statement must not be null"); Assert.notNull(entityConsumer, "Entity Consumer must not be empty"); Assert.notNull(entityClass, "Entity type must not be null"); return getAsyncCqlOperations().query(statement, row -> { entityConsumer.accept(getConverter().read(entityClass, row)); }); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOne(com.datastax.driver.core.Statement, java.lang.Class) */ @Override public <T> ListenableFuture<T> selectOne(Statement statement, Class<T> entityClass) { return new MappingListenableFutureAdapter<>(select(statement, entityClass), list -> list.stream().findFirst().orElse(null)); } // ------------------------------------------------------------------------- // Methods dealing with org.springframework.data.cassandra.core.query.Query // ------------------------------------------------------------------------- /* (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(org.springframework.data.cassandra.core.query.Query, java.lang.Class) */ @Override public <T> ListenableFuture<List<T>> select(Query query, Class<T> entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); return select(getStatementFactory().select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), entityClass); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#select(org.springframework.data.cassandra.core.query.Query, java.util.function.Consumer, java.lang.Class) */ @Override public <T> ListenableFuture<Void> select(Query query, Consumer<T> entityConsumer, Class<T> entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityConsumer, "Entity Consumer must not be empty"); Assert.notNull(entityClass, "Entity type must not be null"); return select(getStatementFactory().select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), entityConsumer, entityClass); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOne(org.springframework.data.cassandra.core.query.Query, java.lang.Class) */ @Override public <T> ListenableFuture<T> selectOne(Query query, Class<T> entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); return selectOne(getStatementFactory().select(query, getMappingContext().getRequiredPersistentEntity(entityClass)), entityClass); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#update(org.springframework.data.cassandra.core.query.Query, org.springframework.data.cassandra.core.query.Update, java.lang.Class) */ @Override public ListenableFuture<Boolean> update(Query query, org.springframework.data.cassandra.core.query.Update update, Class<?> entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(update, "Update must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); return getAsyncCqlOperations().execute(getStatementFactory().update(query, update, getMappingContext().getRequiredPersistentEntity(entityClass))); } /* (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#delete(org.springframework.data.cassandra.core.query.Query, java.lang.Class) */ @Override public ListenableFuture<Boolean> delete(Query query, Class<?> entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); return getAsyncCqlOperations().execute(getStatementFactory().delete(query, getMappingContext().getRequiredPersistentEntity(entityClass))); } // ------------------------------------------------------------------------- // Methods dealing with entities // ------------------------------------------------------------------------- /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#count(java.lang.Class) */ @Override public ListenableFuture<Long> count(Class<?> entityClass) { Assert.notNull(entityClass, "Entity type must not be null"); Select select = QueryBuilder.select().countAll() .from(getMappingContext().getRequiredPersistentEntity(entityClass).getTableName().toCql()); return getAsyncCqlOperations().queryForObject(select, Long.class); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#exists(java.lang.Object, java.lang.Class) */ @Override public ListenableFuture<Boolean> exists(Object id, Class<?> entityClass) { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); CassandraPersistentEntity<?> entity = getMappingContext().getRequiredPersistentEntity(entityClass); Select select = QueryBuilder.select().from(entity.getTableName().toCql()); getConverter().write(id, select.where(), entity); return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().queryForResultSet(select), resultSet -> resultSet.iterator().hasNext()); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#selectOneById(java.lang.Object, java.lang.Class) */ @Override public <T> ListenableFuture<T> selectOneById(Object id, Class<T> entityClass) { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); CassandraPersistentEntity<?> entity = getMappingContext().getRequiredPersistentEntity(entityClass); Select select = QueryBuilder.select().all().from(entity.getTableName().toCql()); getConverter().write(id, select.where(), entity); return selectOne(select, entityClass); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#insert(java.lang.Object) */ @Override public <T> ListenableFuture<T> insert(T entity) { return insert(entity, null); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#insert(java.lang.Object, org.springframework.cassandra.core.WriteOptions) */ @Override public <T> ListenableFuture<T> insert(T entity, WriteOptions options) { Assert.notNull(entity, "Entity must not be null"); Insert insert = QueryUtils.createInsertQuery(getTableName(entity).toCql(), entity, options, getConverter()); return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().execute(new AsyncStatementCallback(insert)), resultSet -> resultSet.wasApplied() ? entity : null); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#update(java.lang.Object) */ @Override public <T> ListenableFuture<T> update(T entity) { return update(entity, null); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#update(java.lang.Object, org.springframework.cassandra.core.WriteOptions) */ @Override public <T> ListenableFuture<T> update(T entity, WriteOptions options) { Assert.notNull(entity, "Entity must not be null"); Update update = QueryUtils.createUpdateQuery(getTableName(entity).toCql(), entity, options, getConverter()); return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().execute(new AsyncStatementCallback(update)), resultSet -> resultSet.wasApplied() ? entity : null); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#delete(java.lang.Object) */ @Override public <T> ListenableFuture<T> delete(T entity) { return delete(entity, null); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#delete(java.lang.Object, org.springframework.cassandra.core.QueryOptions) */ @Override public <T> ListenableFuture<T> delete(T entity, QueryOptions options) { Assert.notNull(entity, "Entity must not be null"); Delete delete = QueryUtils.createDeleteQuery(getTableName(entity).toCql(), entity, options, getConverter()); return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().execute(new AsyncStatementCallback(delete)), resultSet -> resultSet.wasApplied() ? entity : null); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#deleteById(java.lang.Object, java.lang.Class) */ @Override public ListenableFuture<Boolean> deleteById(Object id, Class<?> entityClass) { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); CassandraPersistentEntity<?> entity = getMappingContext().getRequiredPersistentEntity(entityClass); Delete delete = QueryBuilder.delete().from(entity.getTableName().toCql()); getConverter().write(id, delete.where(), entity); return getAsyncCqlOperations().execute(delete); } /* * (non-Javadoc) * @see org.springframework.data.cassandra.core.AsyncCassandraOperations#truncate(java.lang.Class) */ @Override public ListenableFuture<Void> truncate(Class<?> entityClass) { Assert.notNull(entityClass, "Entity type must not be null"); Truncate truncate = QueryBuilder.truncate(getMappingContext().getRequiredPersistentEntity(entityClass).getTableName().toCql()); return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().execute(truncate), aBoolean -> null); } private static class MappingListenableFutureAdapter<T, S> extends org.springframework.util.concurrent.ListenableFutureAdapter<T, S> { private final Function<S, T> mapper; public MappingListenableFutureAdapter(ListenableFuture<S> adaptee, Function<S, T> mapper) { super(adaptee); this.mapper = mapper; } @Override protected T adapt(S adapteeResult) throws ExecutionException { return mapper.apply(adapteeResult); } } private class AsyncStatementCallback implements AsyncSessionCallback<ResultSet>, CqlProvider { private final Statement statement; AsyncStatementCallback(Statement statement) { this.statement = statement; } @Override public ListenableFuture<ResultSet> doInSession(Session session) throws DriverException, DataAccessException { return new GuavaListenableFutureAdapter<>(session.executeAsync(statement), e -> (e instanceof DriverException ? exceptionTranslator.translate("AsyncStatementCallback", getCql(), (DriverException) e) : exceptionTranslator.translateExceptionIfPossible(e))); } @Override public String getCql() { return statement.toString(); } } }