diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java index f66e534bc..1fc740229 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java @@ -86,7 +86,7 @@ public abstract class AbstractCassandraConfiguration extends AbstractSessionConf } /** - * Returns the a {@link CassandraManagedTypes} object holding the initial entity set. + * Returns the given {@link CassandraManagedTypes} object holding the initial entity set. * * @return new instance of {@link CassandraManagedTypes}. * @throws ClassNotFoundException diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java index 96efb6a05..6d06d7640 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraOperations.java @@ -16,6 +16,7 @@ package org.springframework.data.cassandra.core; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import org.springframework.dao.DataAccessException; @@ -27,7 +28,6 @@ import org.springframework.data.cassandra.core.query.CassandraPageRequest; import org.springframework.data.cassandra.core.query.Query; import org.springframework.data.cassandra.core.query.Update; import org.springframework.data.domain.Slice; -import org.springframework.util.concurrent.ListenableFuture; import com.datastax.oss.driver.api.core.cql.AsyncResultSet; import com.datastax.oss.driver.api.core.cql.Statement; @@ -74,7 +74,7 @@ public interface AsyncCassandraOperations { * @return the converted results * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture> select(String cql, Class entityClass) throws DataAccessException; + CompletableFuture> select(String cql, Class entityClass) throws DataAccessException; /** * Execute a {@code SELECT} query and convert the resulting items notifying {@link Consumer} for each entity. @@ -86,7 +86,7 @@ public interface AsyncCassandraOperations { * @return the completion handle * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture select(String cql, Consumer entityConsumer, Class entityClass) + CompletableFuture select(String cql, Consumer entityConsumer, Class entityClass) throws DataAccessException; /** @@ -97,14 +97,14 @@ public interface AsyncCassandraOperations { * @return the converted object or {@literal null}. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture selectOne(String cql, Class entityClass) throws DataAccessException; + CompletableFuture selectOne(String cql, Class entityClass) throws DataAccessException; // ------------------------------------------------------------------------- // Methods dealing with com.datastax.oss.driver.api.core.cql.Statement // ------------------------------------------------------------------------- /** - * Execute the a Cassandra {@link Statement}. Any errors that result from executing this command will be converted + * Execute the given Cassandra {@link Statement}. Any errors that result from executing this command will be converted * into Spring's DAO exception hierarchy. * * @param statement a Cassandra {@link Statement}, must not be {@literal null}. @@ -112,7 +112,7 @@ public interface AsyncCassandraOperations { * @throws DataAccessException if there is any problem executing the query. * @since 3.2 */ - ListenableFuture execute(Statement statement) throws DataAccessException; + CompletableFuture execute(Statement statement) throws DataAccessException; /** * Execute a {@code SELECT} query and convert the resulting items to a {@link List} of entities. @@ -122,7 +122,7 @@ public interface AsyncCassandraOperations { * @return the converted results * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture> select(Statement statement, Class entityClass) throws DataAccessException; + CompletableFuture> select(Statement statement, Class entityClass) throws DataAccessException; /** * Execute a {@code SELECT} query with paging and convert the result set to a {@link Slice} of entities. A sliced @@ -134,7 +134,7 @@ public interface AsyncCassandraOperations { * @throws DataAccessException if there is any problem executing the query. * @see CassandraPageRequest */ - ListenableFuture> slice(Statement statement, Class entityClass) throws DataAccessException; + CompletableFuture> slice(Statement statement, Class entityClass) throws DataAccessException; /** * Execute a {@code SELECT} query and convert the resulting items notifying {@link Consumer} for each entity. @@ -146,7 +146,7 @@ public interface AsyncCassandraOperations { * @return the completion handle * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture select(Statement statement, Consumer entityConsumer, Class entityClass) + CompletableFuture select(Statement statement, Consumer entityConsumer, Class entityClass) throws DataAccessException; /** @@ -157,7 +157,7 @@ public interface AsyncCassandraOperations { * @return the converted object or {@literal null}. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture selectOne(Statement statement, Class entityClass) throws DataAccessException; + CompletableFuture selectOne(Statement statement, Class entityClass) throws DataAccessException; // ------------------------------------------------------------------------- // Methods dealing with org.springframework.data.cassandra.core.query.Query @@ -171,7 +171,7 @@ public interface AsyncCassandraOperations { * @return the converted results * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture> select(Query query, Class entityClass) throws DataAccessException; + CompletableFuture> select(Query query, Class entityClass) throws DataAccessException; /** * Execute a {@code SELECT} query with paging and convert the result set to a {@link Slice} of entities. @@ -182,7 +182,7 @@ public interface AsyncCassandraOperations { * @throws DataAccessException if there is any problem executing the query. * @see CassandraPageRequest */ - ListenableFuture> slice(Query query, Class entityClass) throws DataAccessException; + CompletableFuture> slice(Query query, Class entityClass) throws DataAccessException; /** * Execute a {@code SELECT} query and convert the resulting items notifying {@link Consumer} for each entity. @@ -194,7 +194,7 @@ public interface AsyncCassandraOperations { * @return the completion handle * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture select(Query query, Consumer entityConsumer, Class entityClass) + CompletableFuture select(Query query, Consumer entityConsumer, Class entityClass) throws DataAccessException; /** @@ -205,7 +205,7 @@ public interface AsyncCassandraOperations { * @return the converted object or {@literal null}. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture selectOne(Query query, Class entityClass) throws DataAccessException; + CompletableFuture selectOne(Query query, Class entityClass) throws DataAccessException; /** * Update the queried entities and return {@literal true} if the update was applied. @@ -215,7 +215,7 @@ public interface AsyncCassandraOperations { * @param entityClass The entity type must not be {@literal null}. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture update(Query query, Update update, Class entityClass) throws DataAccessException; + CompletableFuture update(Query query, Update update, Class entityClass) throws DataAccessException; /** * Remove entities (rows)/columns from the table by {@link Query}. @@ -225,7 +225,7 @@ public interface AsyncCassandraOperations { * @return {@literal true} if the deletion was applied. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture delete(Query query, Class entityClass) throws DataAccessException; + CompletableFuture delete(Query query, Class entityClass) throws DataAccessException; // ------------------------------------------------------------------------- // Methods dealing with entities @@ -238,7 +238,7 @@ public interface AsyncCassandraOperations { * @return the number of existing entities. * @throws DataAccessException if any problem occurs while executing the query. */ - ListenableFuture count(Class entityClass) throws DataAccessException; + CompletableFuture count(Class entityClass) throws DataAccessException; /** * Returns the number of rows for the given entity class applying {@link Query}. This overridden method allows users @@ -251,7 +251,7 @@ public interface AsyncCassandraOperations { * @throws DataAccessException if any problem occurs while executing the query. * @since 2.1 */ - ListenableFuture count(Query query, Class entityClass) throws DataAccessException; + CompletableFuture count(Query query, Class entityClass) throws DataAccessException; /** * Determine whether a row of {@code entityClass} with the given {@code id} exists. @@ -263,7 +263,7 @@ public interface AsyncCassandraOperations { * @return {@literal true} if the object exists. * @throws DataAccessException if any problem occurs while executing the query. */ - ListenableFuture exists(Object id, Class entityClass) throws DataAccessException; + CompletableFuture exists(Object id, Class entityClass) throws DataAccessException; /** * Determine whether the result for {@code entityClass} {@link Query} yields at least one row. @@ -274,7 +274,7 @@ public interface AsyncCassandraOperations { * @throws DataAccessException if any problem occurs while executing the query. * @since 2.1 */ - ListenableFuture exists(Query query, Class entityClass) throws DataAccessException; + CompletableFuture exists(Query query, Class entityClass) throws DataAccessException; /** * Execute the Select by {@code id} for the given {@code entityClass}. @@ -286,7 +286,7 @@ public interface AsyncCassandraOperations { * @return the converted object or {@literal null}. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture selectOneById(Object id, Class entityClass) throws DataAccessException; + CompletableFuture selectOneById(Object id, Class entityClass) throws DataAccessException; /** * Insert the given entity and return the entity if the insert was applied. @@ -295,7 +295,7 @@ public interface AsyncCassandraOperations { * @return the inserted entity. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture insert(T entity) throws DataAccessException; + CompletableFuture insert(T entity) throws DataAccessException; /** * Insert the given entity applying {@link WriteOptions} and return the entity if the insert was applied. @@ -306,7 +306,7 @@ public interface AsyncCassandraOperations { * @throws DataAccessException if there is any problem executing the query. * @see InsertOptions#empty() */ - ListenableFuture> insert(T entity, InsertOptions options) throws DataAccessException; + CompletableFuture> insert(T entity, InsertOptions options) throws DataAccessException; /** * Update the given entity and return the entity if the update was applied. @@ -315,7 +315,7 @@ public interface AsyncCassandraOperations { * @return the updated entity. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture update(T entity) throws DataAccessException; + CompletableFuture update(T entity) throws DataAccessException; /** * Update the given entity applying {@link WriteOptions} and return the entity if the update was applied. @@ -326,19 +326,19 @@ public interface AsyncCassandraOperations { * @throws DataAccessException if there is any problem executing the query. * @see UpdateOptions#empty() */ - ListenableFuture> update(T entity, UpdateOptions options) throws DataAccessException; + CompletableFuture> update(T entity, UpdateOptions options) throws DataAccessException; /** - * Delete the given entity and return the entity if the delete was applied. + * Delete the given entity and return the entity if the delete statement was applied. * * @param entity must not be {@literal null}. * @return the deleted entity. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture delete(T entity) throws DataAccessException; + CompletableFuture delete(T entity) throws DataAccessException; /** - * Delete the given entity applying {@link QueryOptions} and return the entity if the delete was applied. + * Delete the given entity applying {@link QueryOptions} and return the entity if the delete statement was applied. * * @param entity must not be {@literal null}. * @param options must not be {@literal null}. @@ -346,10 +346,10 @@ public interface AsyncCassandraOperations { * @throws DataAccessException if there is any problem executing the query. * @see QueryOptions#empty() */ - ListenableFuture delete(Object entity, QueryOptions options) throws DataAccessException; + CompletableFuture delete(Object entity, QueryOptions options) throws DataAccessException; /** - * Delete the given entity applying {@link DeleteOptions} and return the entity if the delete was applied. + * Delete the given entity applying {@link DeleteOptions} and return the entity if the delete statement was applied. * * @param entity must not be {@literal null}. * @param options must not be {@literal null}. @@ -358,7 +358,7 @@ public interface AsyncCassandraOperations { * @see DeleteOptions#empty() * @since 2.2 */ - default ListenableFuture delete(Object entity, DeleteOptions options) throws DataAccessException { + default CompletableFuture delete(Object entity, DeleteOptions options) throws DataAccessException { return delete(entity, (QueryOptions) options); } @@ -372,7 +372,7 @@ public interface AsyncCassandraOperations { * @return {@literal true} if the deletion was applied. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture deleteById(Object id, Class entityClass) throws DataAccessException; + CompletableFuture deleteById(Object id, Class entityClass) throws DataAccessException; /** * Execute a {@code TRUNCATE} query to remove all entities of a given class. @@ -380,6 +380,6 @@ public interface AsyncCassandraOperations { * @param entityClass The entity type must not be {@literal null}. * @throws DataAccessException if there is any problem executing the query. */ - ListenableFuture truncate(Class entityClass) throws DataAccessException; + CompletableFuture truncate(Class entityClass) throws DataAccessException; } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java index d57d545d7..876a3e99c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/AsyncCassandraTemplate.java @@ -17,6 +17,7 @@ package org.springframework.data.cassandra.core; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; import java.util.function.Function; @@ -35,14 +36,12 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.dao.DataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.support.DataAccessUtils; -import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.cassandra.SessionFactory; import org.springframework.data.cassandra.core.EntityOperations.AdaptibleEntity; import org.springframework.data.cassandra.core.convert.CassandraConverter; import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.cql.*; import org.springframework.data.cassandra.core.cql.session.DefaultSessionFactory; -import org.springframework.data.cassandra.core.cql.util.CassandraFutureAdapter; import org.springframework.data.cassandra.core.cql.util.StatementBuilder; import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver; @@ -63,9 +62,7 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; -import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.CqlSession; @@ -119,8 +116,6 @@ public class AsyncCassandraTemplate private final CassandraConverter converter; - private final CqlExceptionTranslator exceptionTranslator; - private final EntityOperations entityOperations; private final StatementFactory statementFactory; @@ -189,7 +184,6 @@ public class AsyncCassandraTemplate this.converter = converter; this.cqlOperations = asyncCqlTemplate; this.entityOperations = new EntityOperations(converter); - this.exceptionTranslator = asyncCqlTemplate.getExceptionTranslator(); this.statementFactory = new StatementFactory(converter); this.eventDelegate = new EntityLifecycleEventDelegate(); } @@ -314,7 +308,7 @@ public class AsyncCassandraTemplate // ------------------------------------------------------------------------- @Override - public ListenableFuture> select(String cql, Class entityClass) { + public CompletableFuture> select(String cql, Class entityClass) { Assert.hasText(cql, "CQL must not be empty"); @@ -322,7 +316,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture select(String cql, Consumer entityConsumer, Class entityClass) + public CompletableFuture select(String cql, Consumer entityConsumer, Class entityClass) throws DataAccessException { Assert.hasText(cql, "CQL must not be empty"); @@ -333,7 +327,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture selectOne(String cql, Class entityClass) { + public CompletableFuture selectOne(String cql, Class entityClass) { Assert.hasText(cql, "CQL must not be empty"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -346,7 +340,7 @@ public class AsyncCassandraTemplate // ------------------------------------------------------------------------- @Override - public ListenableFuture execute(Statement statement) throws DataAccessException { + public CompletableFuture execute(Statement statement) throws DataAccessException { Assert.notNull(statement, "Statement must not be null"); @@ -354,7 +348,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture> select(Statement statement, Class entityClass) { + public CompletableFuture> select(Statement statement, Class entityClass) { Assert.notNull(statement, "Statement must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -365,7 +359,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture select(Statement statement, Consumer entityConsumer, Class entityClass) + public CompletableFuture select(Statement statement, Consumer entityConsumer, Class entityClass) throws DataAccessException { Assert.notNull(statement, "Statement must not be null"); @@ -380,22 +374,21 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture selectOne(Statement statement, Class entityClass) { - return new MappingListenableFutureAdapter<>(select(statement, entityClass), - list -> list.isEmpty() ? null : list.get(0)); + public CompletableFuture selectOne(Statement statement, Class entityClass) { + return select(statement, entityClass).thenApply(list -> list.isEmpty() ? null : list.get(0)); } @Override - public ListenableFuture> slice(Statement statement, Class entityClass) { + public CompletableFuture> slice(Statement statement, Class entityClass) { Assert.notNull(statement, "Statement must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); - ListenableFuture resultSet = doQueryForResultSet(statement); + CompletableFuture resultSet = doQueryForResultSet(statement); Function mapper = getMapper(entityClass, entityClass, EntityQueryUtils.getTableName(statement)); - return new MappingListenableFutureAdapter<>(resultSet, + return resultSet.thenApply( rs -> EntityQueryUtils.readSlice(rs, (row, rowNum) -> mapper.apply(row), 0, getEffectivePageSize(statement))); } @@ -404,7 +397,7 @@ public class AsyncCassandraTemplate // ------------------------------------------------------------------------- @Override - public ListenableFuture> select(Query query, Class entityClass) throws DataAccessException { + public CompletableFuture> select(Query query, Class entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -413,7 +406,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture select(Query query, Consumer entityConsumer, Class entityClass) + public CompletableFuture select(Query query, Consumer entityConsumer, Class entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); @@ -425,7 +418,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture selectOne(Query query, Class entityClass) throws DataAccessException { + public CompletableFuture selectOne(Query query, Class entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -435,7 +428,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture> slice(Query query, Class entityClass) throws DataAccessException { + public CompletableFuture> slice(Query query, Class entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -444,7 +437,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture update(Query query, org.springframework.data.cassandra.core.query.Update update, + public CompletableFuture update(Query query, org.springframework.data.cassandra.core.query.Update update, Class entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); @@ -456,7 +449,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture delete(Query query, Class entityClass) throws DataAccessException { + public CompletableFuture delete(Query query, Class entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -464,7 +457,7 @@ public class AsyncCassandraTemplate return doDelete(query, entityClass, getTableName(entityClass)); } - private ListenableFuture doDelete(Query query, Class entityClass, CqlIdentifier tableName) { + private CompletableFuture doDelete(Query query, Class entityClass, CqlIdentifier tableName) { StatementBuilder builder = getStatementFactory().delete(query, getRequiredPersistentEntity(entityClass), tableName); @@ -472,10 +465,9 @@ public class AsyncCassandraTemplate maybeEmitEvent(() -> new BeforeDeleteEvent<>(delete, entityClass, tableName)); - ListenableFuture future = doExecute(delete, AsyncResultSet::wasApplied); + CompletableFuture future = doExecute(delete, AsyncResultSet::wasApplied); - future.addCallback(success -> maybeEmitEvent(() -> new AfterDeleteEvent<>(delete, entityClass, tableName)), - e -> {}); + future.thenAccept(success -> maybeEmitEvent(() -> new AfterDeleteEvent<>(delete, entityClass, tableName))); return future; } @@ -485,7 +477,7 @@ public class AsyncCassandraTemplate // ------------------------------------------------------------------------- @Override - public ListenableFuture count(Class entityClass) { + public CompletableFuture count(Class entityClass) { Assert.notNull(entityClass, "Entity type must not be null"); @@ -493,7 +485,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture count(Query query, Class entityClass) throws DataAccessException { + public CompletableFuture count(Query query, Class entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -501,14 +493,14 @@ public class AsyncCassandraTemplate return doCount(query, entityClass, getTableName(entityClass)); } - ListenableFuture doCount(Query query, Class entityClass, CqlIdentifier tableName) { + CompletableFuture doCount(Query query, Class entityClass, CqlIdentifier tableName) { StatementBuilder countStatement = getStatementFactory() .count(query, getRequiredPersistentEntity(entityClass), tableName); SimpleStatement statement = countStatement.build(); - ListenableFuture result = doExecute(statement, it -> { + CompletableFuture result = doExecute(statement, it -> { SingleColumnRowMapper mapper = SingleColumnRowMapper.newInstance(Long.class); @@ -516,11 +508,11 @@ public class AsyncCassandraTemplate return mapper.mapRow(row, 0); }); - return new MappingListenableFutureAdapter<>(result, it -> it != null ? it : 0L); + return result.thenApply(it -> it != null ? it : 0L); } @Override - public ListenableFuture exists(Object id, Class entityClass) { + public CompletableFuture exists(Object id, Class entityClass) { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -534,7 +526,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture exists(Query query, Class entityClass) throws DataAccessException { + public CompletableFuture exists(Query query, Class entityClass) throws DataAccessException { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -546,7 +538,7 @@ public class AsyncCassandraTemplate } @Override - public ListenableFuture selectOneById(Object id, Class entityClass) { + public CompletableFuture selectOneById(Object id, Class entityClass) { Assert.notNull(id, "Id must not be null"); Assert.notNull(entityClass, "Entity type must not be null"); @@ -556,17 +548,16 @@ public class AsyncCassandraTemplate StatementBuilder selectOneById(Object id, CassandraPersistentEntity persistentEntity, + public StatementBuilder select = getStatementFactory().selectOneById(id, entity, tableName); + Function mapper = getMapper(entityClass, entityClass, tableName); + + return new MappingListenableFutureAdapter<>(doQuery(select.build(), (row, rowNum) -> mapper.apply(row)), + it -> it.isEmpty() ? null : it.get(0)); + } + + @Override + public ListenableFuture insert(T entity) { + return new MappingListenableFutureAdapter<>(insert(entity, InsertOptions.empty()), EntityWriteResult::getEntity); + } + + @Override + public ListenableFuture> insert(T entity, InsertOptions options) { + + Assert.notNull(entity, "Entity must not be null"); + Assert.notNull(options, "InsertOptions must not be null"); + + return doInsert(entity, options, getTableName(entity.getClass())); + } + + private ListenableFuture> doInsert(T entity, WriteOptions options, CqlIdentifier tableName) { + + AdaptibleEntity source = getEntityOperations().forEntity(maybeCallBeforeConvert(entity, tableName), + getConverter().getConversionService()); + CassandraPersistentEntity persistentEntity = getRequiredPersistentEntity(entity.getClass()); + + T entityToUse = source.isVersionedEntity() ? source.initializeVersionProperty() : entity; + + StatementBuilder builder = getStatementFactory().insert(entityToUse, options, persistentEntity, + tableName); + + if (source.isVersionedEntity()) { + + builder.apply(Insert::ifNotExists); + return doInsertVersioned(builder.build(), entityToUse, source, tableName); + } + + return doInsert(builder.build(), entityToUse, source, tableName); + } + + private ListenableFuture> doInsertVersioned(SimpleStatement insert, T entity, + AdaptibleEntity source, CqlIdentifier tableName) { + + return executeSave(entity, tableName, insert, result -> { + + if (!result.wasApplied()) { + throw new OptimisticLockingFailureException( + String.format("Cannot insert entity %s with version %s into table %s as it already exists", entity, + source.getVersion(), tableName)); + } + }); + } + + @SuppressWarnings("unused") + private ListenableFuture> doInsert(SimpleStatement insert, T entity, + AdaptibleEntity source, CqlIdentifier tableName) { + + return executeSave(entity, tableName, insert); + } + + @Override + public ListenableFuture update(T entity) { + return new MappingListenableFutureAdapter<>(update(entity, UpdateOptions.empty()), EntityWriteResult::getEntity); + } + + @Override + public ListenableFuture> update(T entity, UpdateOptions options) { + + Assert.notNull(entity, "Entity must not be null"); + Assert.notNull(options, "UpdateOptions must not be null"); + + AdaptibleEntity source = getEntityOperations().forEntity(entity, getConverter().getConversionService()); + CassandraPersistentEntity persistentEntity = getRequiredPersistentEntity(entity.getClass()); + CqlIdentifier tableName = persistentEntity.getTableName(); + + T entityToUpdate = maybeCallBeforeConvert(entity, tableName); + + return source.isVersionedEntity() ? doUpdateVersioned(entityToUpdate, options, tableName, persistentEntity) + : doUpdate(entityToUpdate, options, tableName, persistentEntity); + } + + private ListenableFuture> doUpdateVersioned(T entity, UpdateOptions options, + CqlIdentifier tableName, CassandraPersistentEntity persistentEntity) { + + AdaptibleEntity source = getEntityOperations().forEntity(entity, getConverter().getConversionService()); + Number previousVersion = source.getVersion(); + T toSave = source.incrementVersion(); + + StatementBuilder update = getStatementFactory().update(toSave, options, persistentEntity, tableName); + source.appendVersionCondition(update, previousVersion); + + return executeSave(toSave, tableName, update.build(), result -> { + + if (!result.wasApplied()) { + throw new OptimisticLockingFailureException( + String.format("Cannot save entity %s with version %s to table %s; Has it been modified meanwhile", toSave, + source.getVersion(), tableName)); + } + }); + } + + private ListenableFuture> doUpdate(T entity, UpdateOptions options, CqlIdentifier tableName, + CassandraPersistentEntity persistentEntity) { + + StatementBuilder update = getStatementFactory().update(entity, options, persistentEntity, tableName); + + return executeSave(entity, tableName, update.build()); + } + + @Override + public ListenableFuture delete(T entity) { + return new MappingListenableFutureAdapter<>(delete(entity, QueryOptions.empty()), writeResult -> entity); + } + + @Override + public ListenableFuture delete(Object entity, QueryOptions options) { + + Assert.notNull(entity, "Entity must not be null"); + Assert.notNull(options, "QueryOptions must not be null"); + + AdaptibleEntity source = getEntityOperations().forEntity(entity, getConverter().getConversionService()); + CassandraPersistentEntity persistentEntity = getRequiredPersistentEntity(entity.getClass()); + CqlIdentifier tableName = persistentEntity.getTableName(); + + return source.isVersionedEntity() ? doDeleteVersioned(entity, options, source, tableName) + : doDelete(entity, options, tableName); + } + + private ListenableFuture doDeleteVersioned(Object entity, QueryOptions options, + AdaptibleEntity source, CqlIdentifier tableName) { + + StatementBuilder delete = getStatementFactory().delete(entity, options, getConverter(), tableName); + + return executeDelete(entity, tableName, source.appendVersionCondition(delete).build(), result -> { + + if (!result.wasApplied()) { + throw new OptimisticLockingFailureException( + String.format("Cannot delete entity %s with version %s in table %s; Has it been modified meanwhile", entity, + source.getVersion(), tableName)); + } + }); + } + + private ListenableFuture doDelete(Object entity, QueryOptions options, CqlIdentifier tableName) { + + StatementBuilder delete = getStatementFactory().delete(entity, options, getConverter(), tableName); + + return executeDelete(entity, tableName, delete.build(), result -> {}); + } + + @Override + public ListenableFuture deleteById(Object id, Class entityClass) { + + Assert.notNull(id, "Id must not be null"); + Assert.notNull(entityClass, "Entity type must not be null"); + + CassandraPersistentEntity entity = getRequiredPersistentEntity(entityClass); + CqlIdentifier tableName = entity.getTableName(); + + StatementBuilder builder = getStatementFactory().deleteById(id, entity, tableName); + SimpleStatement delete = builder.build(); + + maybeEmitEvent(() -> new BeforeDeleteEvent<>(delete, entityClass, tableName)); + + ListenableFuture future = doExecute(delete, AsyncResultSet::wasApplied); + future.addCallback(success -> maybeEmitEvent(() -> new AfterDeleteEvent<>(delete, entityClass, tableName)), + e -> {}); + + return future; + } + + @Override + public ListenableFuture truncate(Class entityClass) { + + Assert.notNull(entityClass, "Entity type must not be null"); + + CqlIdentifier tableName = getTableName(entityClass); + Truncate truncate = QueryBuilder.truncate(tableName); + SimpleStatement statement = truncate.build(); + + maybeEmitEvent(() -> new BeforeDeleteEvent<>(statement, entityClass, tableName)); + + ListenableFuture future = doExecute(statement, AsyncResultSet::wasApplied); + future.addCallback(success -> maybeEmitEvent(() -> new AfterDeleteEvent<>(statement, entityClass, tableName)), + e -> {}); + + return new MappingListenableFutureAdapter<>(future, aBoolean -> null); + } + + // ------------------------------------------------------------------------- + // Implementation hooks and utility methods + // ------------------------------------------------------------------------- + + /** + * Create a new statement-based {@link AsyncPreparedStatementHandler} using the statement passed in. + *

+ * This method allows for the creation to be overridden by subclasses. + * + * @param statement the statement to be prepared. + * @return the new {@link PreparedStatementHandler} to use. + * @since 3.3.3 + */ + protected AsyncPreparedStatementHandler createPreparedStatementHandler(Statement statement) { + return new PreparedStatementHandler(statement, exceptionTranslator); + } + + private ListenableFuture> executeSave(T entity, CqlIdentifier tableName, + SimpleStatement statement) { + + return executeSave(entity, tableName, statement, ignore -> {}); + } + + private ListenableFuture> executeSave(T entity, CqlIdentifier tableName, + SimpleStatement statement, Consumer beforeAfterSaveEvent) { + + maybeEmitEvent(() -> new BeforeSaveEvent<>(entity, tableName, statement)); + T entityToSave = maybeCallBeforeSave(entity, tableName, statement); + + ListenableFuture result = doQueryForResultSet(statement); + + return new MappingListenableFutureAdapter<>(result, resultSet -> { + + EntityWriteResult writeResult = new org.springframework.data.cassandra.core.legacy.EntityWriteResult<>( + Collections.singletonList(resultSet.getExecutionInfo()), resultSet.wasApplied(), getFirstPage(resultSet), + entityToSave); + + beforeAfterSaveEvent.accept(writeResult); + + maybeEmitEvent(() -> new AfterSaveEvent<>(entityToSave, tableName)); + + return writeResult; + }); + } + + private ListenableFuture executeDelete(Object entity, CqlIdentifier tableName, SimpleStatement statement, + Consumer resultConsumer) { + + maybeEmitEvent(() -> new BeforeDeleteEvent<>(statement, entity.getClass(), tableName)); + + ListenableFuture result = doQueryForResultSet(statement); + + return new MappingListenableFutureAdapter<>(result, resultSet -> { + + WriteResult writeResult = new org.springframework.data.cassandra.core.legacy.WriteResult( + Collections.singletonList(resultSet.getExecutionInfo()), resultSet.wasApplied(), getFirstPage(resultSet)); + + resultConsumer.accept(writeResult); + + maybeEmitEvent(() -> new AfterDeleteEvent<>(statement, entity.getClass(), tableName)); + + return writeResult; + }); + } + + private ListenableFuture> doQuery(Statement statement, RowMapper rowMapper) { + + if (PreparedStatementDelegate.canPrepare(isUsePreparedStatements(), statement, log)) { + + AsyncPreparedStatementHandler statementHandler = createPreparedStatementHandler(statement); + return getAsyncCqlOperations().query(statementHandler, statementHandler, rowMapper); + } + + return getAsyncCqlOperations().query(statement, rowMapper); + } + + private ListenableFuture doQuery(Statement statement, RowCallbackHandler callbackHandler) { + + if (PreparedStatementDelegate.canPrepare(isUsePreparedStatements(), statement, log)) { + + AsyncPreparedStatementHandler statementHandler = createPreparedStatementHandler(statement); + return getAsyncCqlOperations().query(statementHandler, statementHandler, callbackHandler); + } + + return getAsyncCqlOperations().query(statement, callbackHandler); + } + + private ListenableFuture doQueryForResultSet(Statement statement) { + return doExecute(statement, Function.identity()); + } + + private ListenableFuture doExecute(Statement statement, Function mappingFunction) { + + if (PreparedStatementDelegate.canPrepare(isUsePreparedStatements(), statement, log)) { + + AsyncPreparedStatementHandler statementHandler = createPreparedStatementHandler(statement); + return getAsyncCqlOperations().query(statementHandler, statementHandler, + (AsyncResultSetExtractor) resultSet -> new AsyncResult<>(mappingFunction.apply(resultSet))); + } + + return new MappingListenableFutureAdapter<>(getAsyncCqlOperations().queryForResultSet(statement), mappingFunction); + } + + private static List getFirstPage(AsyncResultSet resultSet) { + return StreamSupport.stream(resultSet.currentPage().spliterator(), false).collect(Collectors.toList()); + } + + private static int getConfiguredPageSize(CqlSession session) { + return session.getContext().getConfig().getDefaultProfile().getInt(DefaultDriverOption.REQUEST_PAGE_SIZE, 5000); + } + + private int getEffectivePageSize(Statement statement) { + + if (statement.getPageSize() > 0) { + return statement.getPageSize(); + } + + if (getAsyncCqlOperations() instanceof CassandraAccessor) { + + CassandraAccessor accessor = (CassandraAccessor) getAsyncCqlOperations(); + + if (accessor.getFetchSize() != -1) { + return accessor.getFetchSize(); + } + } + class GetConfiguredPageSize implements AsyncSessionCallback, CqlProvider { + @Override + public ListenableFuture doInSession(CqlSession session) { + return AsyncResult.forValue(getConfiguredPageSize(session)); + } + + @Override + public String getCql() { + return QueryExtractorDelegate.getCql(statement); + } + } + + return getAsyncCqlOperations().execute(new GetConfiguredPageSize()).completable().join(); + } + + @SuppressWarnings("unchecked") + private Function getMapper(Class entityType, Class targetType, CqlIdentifier tableName) { + + EntityProjection projection = entityOperations.introspectProjection(targetType, entityType); + + return row -> { + + maybeEmitEvent(() -> new AfterLoadEvent<>(row, targetType, tableName)); + + T result = getConverter().project(projection, row); + + if (result != null) { + maybeEmitEvent(() -> new AfterConvertEvent<>(row, result, tableName)); + } + + return result; + }; + } + + private static MappingCassandraConverter newConverter(CqlSession session) { + + MappingCassandraConverter converter = new MappingCassandraConverter(); + converter.setUserTypeResolver(new SimpleUserTypeResolver(session)); + converter.setCodecRegistry(session.getContext().getCodecRegistry()); + + converter.afterPropertiesSet(); + + return converter; + } + + protected , T> void maybeEmitEvent(Supplier event) { + this.eventDelegate.publishEvent(event); + } + + protected T maybeCallBeforeConvert(T object, CqlIdentifier tableName) { + + if (null != entityCallbacks) { + return entityCallbacks.callback(BeforeConvertCallback.class, object, tableName); + } + + return object; + } + + protected T maybeCallBeforeSave(T object, CqlIdentifier tableName, Statement statement) { + + if (null != entityCallbacks) { + return entityCallbacks.callback(BeforeSaveCallback.class, object, tableName, statement); + } + + return object; + } + + static class MappingListenableFutureAdapter + extends org.springframework.util.concurrent.ListenableFutureAdapter { + + private final Function mapper; + + MappingListenableFutureAdapter(ListenableFuture adaptee, Function mapper) { + super(adaptee); + this.mapper = mapper; + } + + @Override + protected T adapt(@Nullable S adapteeResult) { + return this.mapper.apply(adapteeResult); + } + } + + /** + * General callback interface used to create and bind prepared CQL statements. + *

+ * This interface prepares the CQL statement and sets values on a {@link PreparedStatement} as union-type comprised + * from {@link AsyncPreparedStatementCreator}, {@link PreparedStatementBinder}, and {@link CqlProvider}. + * + * @since 3.3.3 + */ + public interface AsyncPreparedStatementHandler + extends AsyncPreparedStatementCreator, PreparedStatementBinder, CqlProvider { + + } + + /** + * Utility class to prepare a {@link SimpleStatement} and bind values associated with the statement to a + * {@link BoundStatement}. + * + * @since 3.2 + */ + public static class PreparedStatementHandler implements AsyncPreparedStatementHandler { + + private final SimpleStatement statement; + private final PersistenceExceptionTranslator exceptionTranslator; + + public PreparedStatementHandler(Statement statement, PersistenceExceptionTranslator exceptionTranslator) { + this.statement = PreparedStatementDelegate.getStatementForPrepare(statement); + this.exceptionTranslator = exceptionTranslator; + } + + @Override + public ListenableFuture createPreparedStatement(CqlSession session) throws DriverException { + return new CassandraFutureAdapter<>(doPrepare(session), exceptionTranslator); + } + + /** + * Invokes the statement preparation. + * + * @param session + * @return + */ + protected CompletionStage doPrepare(CqlSession session) { + return session.prepareAsync(statement.getQuery()); + } + + @Override + public BoundStatement bindValues(PreparedStatement ps) throws DriverException { + return PreparedStatementDelegate.bind(statement, ps); + } + + @Override + public String getCql() { + return statement.getQuery(); + } + + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityLifecycleEventDelegate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityLifecycleEventDelegate.java new file mode 100644 index 000000000..544d2f32c --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityLifecycleEventDelegate.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 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 + * + * https://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.legacy; + +import java.util.function.Supplier; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.lang.Nullable; + +/** + * Delegate class to encapsulate lifecycle event configuration and publishing. Event creation is deferred within an + * event {@link Supplier} to delay the actual event object creation. + * + * @author Mark Paluch + * @since 4.0 + * @see ApplicationEventPublisher + */ +@Deprecated +class EntityLifecycleEventDelegate { + + private @Nullable ApplicationEventPublisher publisher; + private boolean eventsEnabled = true; + + public void setPublisher(@Nullable ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + public boolean isEventsEnabled() { + return eventsEnabled; + } + + public void setEventsEnabled(boolean eventsEnabled) { + this.eventsEnabled = eventsEnabled; + } + + /** + * Publish an application event if event publishing is enabled. + * + * @param eventSupplier the supplier for application events. + */ + public void publishEvent(Supplier eventSupplier) { + + if (canPublishEvent()) { + publisher.publishEvent(eventSupplier.get()); + } + } + + private boolean canPublishEvent() { + return publisher != null && eventsEnabled; + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityOperations.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityOperations.java new file mode 100644 index 000000000..402352639 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityOperations.java @@ -0,0 +1,359 @@ +/* + * Copyright 2019-2022 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 + * + * https://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.legacy; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.core.ReactiveCassandraTemplate; +import org.springframework.data.cassandra.core.convert.CassandraConverter; +import org.springframework.data.cassandra.core.cql.util.StatementBuilder; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.projection.EntityProjection; +import org.springframework.data.projection.EntityProjectionIntrospector; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.condition.Condition; +import com.datastax.oss.driver.api.querybuilder.delete.Delete; +import com.datastax.oss.driver.api.querybuilder.update.Update; + +/** + * Common data access operations performed on an entity using a {@link MappingContext} containing mapping metadata. + * + * @author Mark Paluch + * @author John Blum + * @see CassandraTemplate + * @see AsyncCassandraTemplate + * @see ReactiveCassandraTemplate + * @since 4.0 + */ +@Deprecated(since = "4.0", forRemoval = true) +class EntityOperations { + + private final MappingContext, CassandraPersistentProperty> mappingContext; + private final EntityProjectionIntrospector introspector; + + EntityOperations(CassandraConverter converter) { + this(converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory()); + } + + EntityOperations(MappingContext, CassandraPersistentProperty> context, + CustomConversions conversions, ProjectionFactory projectionFactory) { + this.mappingContext = context; + this.introspector = EntityProjectionIntrospector.create(projectionFactory, + EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() + .and(((target, underlyingType) -> !conversions.isSimpleType(target))), + context); + } + + /** + * Creates a new {@link Entity} for the given bean. + * + * @param entity must not be {@literal null}. + * @return + */ + public Entity forEntity(T entity) { + + Assert.notNull(entity, "Bean must not be null"); + + return MappedEntity.of(entity, getMappingContext()); + } + + /** + * Creates a new {@link AdaptibleEntity} for the given bean and {@link ConversionService}. + * + * @param entity must not be {@literal null}. + * @param conversionService must not be {@literal null}. + * @return + */ + public AdaptibleEntity forEntity(T entity, ConversionService conversionService) { + + Assert.notNull(entity, "Bean must not be null"); + Assert.notNull(conversionService, "ConversionService must not be null"); + + return AdaptibleMappedEntity.of(entity, getMappingContext(), conversionService); + } + + /** + * Returns the {@link MappingContext} used by this entity data access operations class to access mapping meta-data + * used to store (map) object to Cassandra tables. + * + * @return the {@link MappingContext} used by this entity data access operations class. + * @see org.springframework.data.cassandra.core.mapping.CassandraMappingContext + */ + CassandraPersistentEntity getRequiredPersistentEntity(Class entityClass) { + return getMappingContext().getRequiredPersistentEntity(ClassUtils.getUserClass(entityClass)); + } + + /** + * Returns the table name to which the entity shall be persisted. + * + * @param entityClass entity class, must not be {@literal null}. + * @return the table name to which the entity shall be persisted. + */ + CqlIdentifier getTableName(Class entityClass) { + return getRequiredPersistentEntity(entityClass).getTableName(); + } + + /** + * Introspect the given {@link Class result type} in the context of the {@link Class entity type} whether the returned + * type is a projection and what property paths are participating in the projection. + * + * @param resultType the type to project on. Must not be {@literal null}. + * @param entityType the source domain type. Must not be {@literal null}. + * @return the introspection result. + * @since 3.4 + * @see EntityProjectionIntrospector#introspect(Class, Class) + */ + public EntityProjection introspectProjection(Class resultType, Class entityType) { + return introspector.introspect(resultType, entityType); + } + + protected MappingContext, CassandraPersistentProperty> getMappingContext() { + return this.mappingContext; + } + + /** + * A representation of information about an entity. + */ + interface Entity { + + /** + * Returns whether the entity is versioned, i.e. if it contains a version property. + * + * @return + */ + default boolean isVersionedEntity() { + return false; + } + + /** + * Returns the value of the version if the entity has a version property, {@literal null} otherwise. + * + * @return + */ + @Nullable + Object getVersion(); + + /** + * Returns the underlying bean. + * + * @return + */ + T getBean(); + + /** + * Returns whether the entity is considered to be new. + * + * @return + */ + boolean isNew(); + } + + /** + * Information and commands on an entity. + */ + interface AdaptibleEntity extends Entity { + + /** + * Appends a {@code IF} condition to an {@link Update} statement for optimistic locking to perform the update only + * if the version number matches. This method accepts {@code currentVersionNumber} as the {@link Update} typically + * requires to increment the version number upon assembly time. + * + * @param update the {@link Update} statement to append the condition to. + * @param currentVersionNumber previous version number. + * @return the altered {@link Update} containing the {@code IF} condition for optimistic locking. + */ + StatementBuilder appendVersionCondition(StatementBuilder update, Number currentVersionNumber); + + /** + * Appends a {@code IF} condition to an {@link Delete} statement for optimistic locking to perform the delete only + * if the version number matches. The {@link #getVersion() version number} is derived from the actual state as + * delete statements typically do not increment the version prior to statement creation. + * + * @param delete the {@link Delete} statement to append the condition to. + * @return the altered {@link Delete} containing the {@code IF} condition for optimistic locking. + * @see #getVersion() + */ + StatementBuilder appendVersionCondition(StatementBuilder delete); + + /** + * Initializes the version property of the of the current entity if available. + * + * @return the entity with the version property updated if available. + */ + T initializeVersionProperty(); + + /** + * Increments the value of the version property if available. + * + * @return the entity with the version property incremented if available. + */ + T incrementVersion(); + + /** + * Returns the current version value if the entity has a version property. + * + * @return the current version or {@literal null} in case it's uninitialized or the entity doesn't expose a version + * property. + */ + @Nullable + Number getVersion(); + + /** + * Returns the {@link CassandraPersistentEntity}. + * + * @return the {@link CassandraPersistentEntity}. + */ + CassandraPersistentEntity getPersistentEntity(); + + } + + private static class MappedEntity implements Entity { + + private final CassandraPersistentEntity entity; + private final PersistentPropertyAccessor propertyAccessor; + + protected MappedEntity(CassandraPersistentEntity entity, PersistentPropertyAccessor propertyAccessor) { + this.entity = entity; + this.propertyAccessor = propertyAccessor; + } + + private static MappedEntity of(T bean, + MappingContext, CassandraPersistentProperty> context) { + + CassandraPersistentEntity entity = context.getRequiredPersistentEntity(bean.getClass()); + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(bean); + + return new MappedEntity<>(entity, propertyAccessor); + } + + @Override + public T getBean() { + return this.propertyAccessor.getBean(); + } + + @Override + public boolean isNew() { + return this.entity.isNew(getBean()); + } + + @Override + public boolean isVersionedEntity() { + return this.entity.hasVersionProperty(); + } + + @Override + @Nullable + public Object getVersion() { + return this.propertyAccessor.getProperty(this.entity.getRequiredVersionProperty()); + } + } + + private static class AdaptibleMappedEntity extends MappedEntity implements AdaptibleEntity { + + private final CassandraPersistentEntity entity; + private final ConvertingPropertyAccessor propertyAccessor; + + private static AdaptibleEntity of(T bean, + MappingContext, CassandraPersistentProperty> mappingContext, + ConversionService conversionService) { + + CassandraPersistentEntity entity = mappingContext.getRequiredPersistentEntity(bean.getClass()); + + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(bean); + + return new AdaptibleMappedEntity<>(entity, new ConvertingPropertyAccessor<>(propertyAccessor, conversionService)); + } + + private AdaptibleMappedEntity(CassandraPersistentEntity entity, ConvertingPropertyAccessor propertyAccessor) { + + super(entity, propertyAccessor); + + this.entity = entity; + this.propertyAccessor = propertyAccessor; + } + + @Override + public StatementBuilder appendVersionCondition(StatementBuilder update, + Number currentVersionNumber) { + + return update.bind((statement, factory) -> { + return statement.if_(Condition.column(getVersionColumnName()).isEqualTo(factory.create(currentVersionNumber))); + }); + } + + @Override + public StatementBuilder appendVersionCondition(StatementBuilder delete) { + + return delete.bind((statement, factory) -> { + return statement.if_(Condition.column(getVersionColumnName()).isEqualTo(factory.create(getVersion()))); + }); + } + + @Override + public T initializeVersionProperty() { + + if (this.entity.hasVersionProperty()) { + + CassandraPersistentProperty versionProperty = this.entity.getRequiredVersionProperty(); + + this.propertyAccessor.setProperty(versionProperty, versionProperty.getType().isPrimitive() ? 1 : 0); + } + + return this.propertyAccessor.getBean(); + } + + @Override + public T incrementVersion() { + + CassandraPersistentProperty versionProperty = this.entity.getRequiredVersionProperty(); + + Number version = getVersion(); + Number nextVersion = version == null ? 0 : version.longValue() + 1; + + this.propertyAccessor.setProperty(versionProperty, nextVersion); + + return this.propertyAccessor.getBean(); + } + + @Override + @Nullable + public Number getVersion() { + + CassandraPersistentProperty versionProperty = this.entity.getRequiredVersionProperty(); + + return this.propertyAccessor.getProperty(versionProperty, Number.class); + } + + @Override + public CassandraPersistentEntity getPersistentEntity() { + return this.entity; + } + + private CqlIdentifier getVersionColumnName() { + return this.entity.getRequiredVersionProperty().getColumnName(); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityQueryUtils.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityQueryUtils.java new file mode 100644 index 000000000..429ab2112 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityQueryUtils.java @@ -0,0 +1,184 @@ +/* + * Copyright 2016-2022 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 + * + * https://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.legacy; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.data.cassandra.core.cql.RowMapper; +import org.springframework.data.cassandra.core.query.CassandraPageRequest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.lang.Nullable; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; + +/** + * Simple utility class for working with the QueryBuilder API using mapped entities. + *

+ * Only intended for internal use. + * + * @author Mark Paluch + * @since 4.0 + */ +@Deprecated(since = "4.0", forRemoval = true) +class EntityQueryUtils { + + private static final Pattern FROM_REGEX = Pattern.compile(" FROM ([\"]?[\\w]*[\\\\.]?[\\w]*[\"]?)[\\s]?", + Pattern.CASE_INSENSITIVE); + + /** + * Read a {@link Slice} of data from the {@link ResultSet} for a {@link Pageable}. + * + * @param resultSet must not be {@literal null}. + * @param mapper must not be {@literal null}. + * @param page + * @param pageSize + * @return the resulting {@link Slice}. + */ + static Slice readSlice(ResultSet resultSet, RowMapper mapper, int page, int pageSize) { + + int toRead = resultSet.getAvailableWithoutFetching(); + + return readSlice(() -> limit(resultSet.iterator(), toRead), resultSet.getExecutionInfo().getPagingState(), mapper, + page, pageSize); + } + + /** + * Read a {@link Slice} of data from the {@link ResultSet} for a {@link Pageable}. + * + * @param resultSet must not be {@literal null}. + * @param mapper must not be {@literal null}. + * @param page + * @param pageSize + * @return the resulting {@link Slice}. + * @since 3.0 + */ + static Slice readSlice(AsyncResultSet resultSet, RowMapper mapper, int page, int pageSize) { + + return readSlice(() -> limit(resultSet.currentPage().iterator(), resultSet.remaining()), + resultSet.getExecutionInfo().getPagingState(), mapper, page, pageSize); + } + + /** + * Read a {@link Slice} of data from the {@link Iterable} of {@link Row}s for a {@link Pageable}. + * + * @param rows must not be {@literal null}. + * @param pagingState + * @param mapper must not be {@literal null}. + * @param page + * @param pageSize + * @return the resulting {@link Slice}. + * @since 2.1 + */ + static Slice readSlice(Iterable rows, @Nullable ByteBuffer pagingState, RowMapper mapper, int page, + int pageSize) { + + List result = new ArrayList<>(pageSize); + + Iterator iterator = rows.iterator(); + int index = 0; + + while (iterator.hasNext()) { + T element = mapper.mapRow(iterator.next(), index++); + result.add(element); + } + + CassandraPageRequest pageRequest = CassandraPageRequest.of(PageRequest.of(page, pageSize), pagingState); + + return new SliceImpl<>(result, pageRequest, pagingState != null); + } + + /** + * Extract the table name from a {@link Statement}. + * + * @param statement + * @return + * @since 2.1 + */ + static CqlIdentifier getTableName(Statement statement) { + + String cql = statement instanceof SimpleStatement ? ((SimpleStatement) statement).getQuery() : statement.toString(); + Matcher matcher = FROM_REGEX.matcher(cql); + + if (matcher.find()) { + + String cqlTableName = matcher.group(1); + + int separator = cqlTableName.indexOf('.'); + + if (separator != -1) { + cqlTableName = cqlTableName.substring(separator + 1); + } + + if (cqlTableName.startsWith("\"") || cqlTableName.endsWith("\"")) { + return CqlIdentifier.fromCql(cqlTableName.substring(separator + 1)); + } + + return CqlIdentifier.fromInternal(cqlTableName); + } + + return CqlIdentifier.fromCql("unknown"); + } + + /** + * Returns a view containing the first {@code limitSize} elements of {@code iterator}. If {@code + * iterator} contains fewer than {@code limitSize} elements, the returned view contains all of its elements. The + * returned iterator supports {@code remove()} if {@code iterator} does. + * + * @param iterator the iterator to limit + * @param limitSize the maximum number of elements in the returned iterator + * @throws IllegalArgumentException if {@code limitSize} is negative + * @since 3.0 + */ + private static Iterator limit(Iterator iterator, int limitSize) { + + return new Iterator() { + private int count; + + @Override + public boolean hasNext() { + return count < limitSize && iterator.hasNext(); + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + count++; + return iterator.next(); + } + + @Override + public void remove() { + iterator.remove(); + } + }; + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityWriteResult.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityWriteResult.java new file mode 100644 index 000000000..f50b8e39e --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/EntityWriteResult.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018-2022 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 + * + * https://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.legacy; + +import java.util.List; + +import org.springframework.data.cassandra.core.WriteResult; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Row; + +/** + * The result of a write operation for an entity. + * + * @author Mark Paluch + * @see WriteResult + */ +@Deprecated(since = "4.0", forRemoval = true) +class EntityWriteResult extends org.springframework.data.cassandra.core.EntityWriteResult { + + public EntityWriteResult(List executionInfo, boolean wasApplied, List rows, T entity) { + super(executionInfo, wasApplied, rows, entity); + } + +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/PreparedStatementDelegate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/PreparedStatementDelegate.java new file mode 100644 index 000000000..fa0f04249 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/PreparedStatementDelegate.java @@ -0,0 +1,292 @@ +/* + * Copyright 2020-2022 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 + * + * https://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.legacy; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; + +import org.springframework.data.cassandra.core.cql.QueryExtractorDelegate; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.function.SingletonSupplier; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; + +/** + * Support class for Cassandra Template API implementation classes that want to make use of prepared statements. + * + * @author Mark Paluch + * @since 4.0 + */ +@Deprecated(since = "4.0", forRemoval = true) +class PreparedStatementDelegate { + + /** + * Bind values held in {@link SimpleStatement} to the {@link PreparedStatement} and apply query options that are set + * or do not match the default value. + * + * @param source + * @param ps + * @return the bound statement. + */ + static BoundStatement bind(SimpleStatement source, PreparedStatement ps) { + + BoundStatementBuilder builder = ps.boundStatementBuilder(source.getPositionalValues().toArray()); + + Mapper mapper = Mapper.INSTANCE; + + mapper.from(source.getExecutionProfileName()).whenHasText().to(builder::setExecutionProfileName); + mapper.from(source.getExecutionProfile()).whenNonNull().to(builder::setExecutionProfile); + mapper.from(source.getRoutingKeyspace()).whenNonNull().to(builder::setRoutingKeyspace); + mapper.from(source.getRoutingKey()).whenNonNull().to(builder::setRoutingKey); + mapper.from(source.getRoutingToken()).whenNonNull().to(builder::setRoutingToken); + mapper.from(source.isIdempotent()).whenNonNull().to(builder::setIdempotence); + mapper.from(source.isTracing()).whenNonNull().to(builder::setTracing); + mapper.from(source.getQueryTimestamp()).whenNot(it -> it == Statement.NO_DEFAULT_TIMESTAMP) + .to(builder::setQueryTimestamp); + mapper.from(source.getPagingState()).whenNonNull().to(builder::setPagingState); + mapper.from(source.getPageSize()).whenNot(it -> it == 0L).to(builder::setPageSize); + mapper.from(source.getConsistencyLevel()).whenNonNull().to(builder::setConsistencyLevel); + mapper.from(source.getSerialConsistencyLevel()).whenNonNull().to(builder::setSerialConsistencyLevel); + mapper.from(source.getTimeout()).whenNonNull().to(builder::setTimeout); + mapper.from(source.getNode()).whenNonNull().to(builder::setNode); + mapper.from(source.getNowInSeconds()).whenNot(it -> it == Statement.NO_NOW_IN_SECONDS).to(builder::setNowInSeconds); + + Map namedValues = source.getNamedValues(); + + ColumnDefinitions variableDefinitions = ps.getVariableDefinitions(); + CodecRegistry codecRegistry = builder.codecRegistry(); + for (Map.Entry entry : namedValues.entrySet()) { + + if (entry.getValue() == null) { + builder = builder.setToNull(entry.getKey()); + } else { + DataType type = variableDefinitions.get(entry.getKey()).getType(); + builder = builder.set(entry.getKey(), entry.getValue(), codecRegistry.codecFor(type)); + } + } + + return builder.build(); + } + + /** + * Ensure the given {@link Statement} is a {@link SimpleStatement}. Throw a {@link IllegalArgumentException} + * otherwise. + * + * @param statement + * @return the {@link SimpleStatement}. + */ + static SimpleStatement getStatementForPrepare(Statement statement) { + + if (statement instanceof SimpleStatement) { + return (SimpleStatement) statement; + } + + throw new IllegalArgumentException(getMessage(statement)); + } + + /** + * Check whether to use prepared statements. When {@code usePreparedStatements} is {@literal true}, then verifying + * additionally that the given {@link Statement} is a {@link SimpleStatement}, otherwise log the mismatch and fallback + * to non-prepared usage. + * + * @param usePreparedStatements + * @param statement + * @param logger + * @return + */ + static boolean canPrepare(boolean usePreparedStatements, Statement statement, Log logger) { + + if (usePreparedStatements) { + + if (statement instanceof SimpleStatement) { + return true; + } + + logger.warn(getMessage(statement)); + } + + return false; + } + + private static String getMessage(Statement statement) { + + String cql = QueryExtractorDelegate.getCql(statement); + + if (StringUtils.hasText(cql)) { + return String.format("Cannot prepare statement %s (%s); Statement must be a SimpleStatement", cql, statement); + } + + return String.format("Cannot prepare statement %s; Statement must be a SimpleStatement", statement); + } + + enum Mapper { + + INSTANCE; + + /** + * Return a new {@link Source} from the specified value supplier that can be used to perform the mapping. + * + * @param the source type + * @param supplier the value supplier + * @return a {@link Source} that can be used to complete the mapping + * @see #from(Object) + */ + public Source from(Supplier supplier) { + + Assert.notNull(supplier, "Supplier must not be null"); + return getSource(supplier); + } + + /** + * Return a new {@link Source} from the specified value that can be used to perform the mapping. + * + * @param the source type + * @param value the value + * @return a {@link Source} that can be used to complete the mapping + */ + public Source from(@Nullable T value) { + return from(() -> value); + } + + private Source getSource(Supplier supplier) { + return new Source<>(SingletonSupplier.of(supplier), t -> true); + } + } + + /** + * A source value/supplier that is in the process of being mapped. + * + * @param the source type + */ + static class Source { + + private final Supplier supplier; + + private final Predicate predicate; + + private Source(Supplier supplier, Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null"); + + this.supplier = supplier; + this.predicate = predicate; + } + + /** + * Return a filtered version of the source that won't map non-null values or suppliers that throw a + * {@link NullPointerException}. + * + * @return a new filtered source instance + */ + public Source whenNonNull() { + return new Source<>(this.supplier, Objects::nonNull); + } + + /** + * Return a filtered version of the source that will only map values that are {@code true}. + * + * @return a new filtered source instance + */ + public Source whenTrue() { + return when(Boolean.TRUE::equals); + } + + /** + * Return a filtered version of the source that will only map values that are {@code false}. + * + * @return a new filtered source instance + */ + public Source whenFalse() { + return when(Boolean.FALSE::equals); + } + + /** + * Return a filtered version of the source that will only map values that have a {@code toString()} containing + * actual text. + * + * @return a new filtered source instance + */ + public Source whenHasText() { + return when((value) -> StringUtils.hasText(Objects.toString(value, null))); + } + + /** + * Return a filtered version of the source that will only map values equal to the specified {@code object}. + * + * @param object the object to match + * @return a new filtered source instance + */ + public Source whenEqualTo(Object object) { + return when(object::equals); + } + + /** + * Return a filtered version of the source that won't map values that match the given predicate. + * + * @param predicate the predicate used to filter values + * @return a new filtered source instance + */ + public Source whenNot(Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null"); + return when(predicate.negate()); + } + + /** + * Return a filtered version of the source that won't map values that don't match the given predicate. + * + * @param predicate the predicate used to filter values + * @return a new filtered source instance + */ + public Source when(Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null"); + return new Source<>(this.supplier, (this.predicate != null) ? this.predicate.and(predicate) : predicate); + } + + /** + * Complete the mapping by passing any non-filtered value to the specified consumer. + * + * @param consumer the consumer that should accept the value if it's not been filtered + */ + public void to(Consumer consumer) { + + Assert.notNull(consumer, "Consumer must not be null"); + + T value = this.supplier.get(); + if (this.predicate.test(value)) { + consumer.accept(value); + } + } + + } + +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/WriteResult.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/WriteResult.java new file mode 100644 index 000000000..cd802b0de --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/WriteResult.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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 + * + * https://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.legacy; + +import java.util.List; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Row; + +@Deprecated(since = "4.0", forRemoval = true) +class WriteResult extends org.springframework.data.cassandra.core.WriteResult { + + WriteResult(List executionInfo, boolean wasApplied, List rows) { + super(executionInfo, wasApplied, rows); + } + +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/package-info.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/package-info.java new file mode 100644 index 000000000..13423e7c5 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/legacy/package-info.java @@ -0,0 +1,7 @@ +/** + * Apache Cassandra legacy asynchronous support for easier migration. + */ +@NonNullApi +package org.springframework.data.cassandra.core.legacy; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/AsyncCassandraOperationsExtensions.kt b/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/AsyncCassandraOperationsExtensions.kt index aade186c3..319e11d6e 100644 --- a/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/AsyncCassandraOperationsExtensions.kt +++ b/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/AsyncCassandraOperationsExtensions.kt @@ -19,7 +19,7 @@ import com.datastax.oss.driver.api.core.cql.Statement import org.springframework.data.cassandra.core.query.Query import org.springframework.data.cassandra.core.query.Update import org.springframework.data.domain.Slice -import org.springframework.util.concurrent.ListenableFuture +import java.util.concurrent.CompletableFuture import kotlin.reflect.KClass /** @@ -36,41 +36,63 @@ import kotlin.reflect.KClass /** * Extension for [AsyncCassandraOperations.select] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("select(cql)")) -fun AsyncCassandraOperations.select(cql: String, entityClass: KClass): ListenableFuture> = - select(cql, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("select(cql)") +) +fun AsyncCassandraOperations.select( + cql: String, + entityClass: KClass +): CompletableFuture> = + select(cql, entityClass.java) /** * Extension for [AsyncCassandraOperations.select] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.select(cql: String): ListenableFuture> = - select(cql, T::class.java) +inline fun AsyncCassandraOperations.select(cql: String): CompletableFuture> = + select(cql, T::class.java) /** * Extension for [AsyncCassandraOperations.select] providing a Consumer-like function. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("select(cql, consumer)")) -fun AsyncCassandraOperations.select(cql: String, entityClass: KClass, consumer: (T) -> Unit): ListenableFuture = - select(cql, consumer, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("select(cql, consumer)") +) +fun AsyncCassandraOperations.select( + cql: String, + entityClass: KClass, + consumer: (T) -> Unit +): CompletableFuture = + select(cql, consumer, entityClass.java) /** * Extension for [AsyncCassandraOperations.select] providing a Consumer-like function leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.select(cql: String, crossinline consumer: (T) -> Unit): ListenableFuture = - select(cql, { consumer(it) }, T::class.java) +inline fun AsyncCassandraOperations.select( + cql: String, + crossinline consumer: (T) -> Unit +): CompletableFuture = + select(cql, { consumer(it) }, T::class.java) /** * Extension for [AsyncCassandraOperations.selectOne] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("selectOne(cql)")) -fun AsyncCassandraOperations.selectOne(cql: String, entityClass: KClass): ListenableFuture = - selectOne(cql, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("selectOne(cql)") +) +fun AsyncCassandraOperations.selectOne( + cql: String, + entityClass: KClass +): CompletableFuture = + selectOne(cql, entityClass.java) /** * Extension for [AsyncCassandraOperations.selectOne] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.selectOne(cql: String): ListenableFuture = - selectOne(cql, T::class.java) +inline fun AsyncCassandraOperations.selectOne(cql: String): CompletableFuture = + selectOne(cql, T::class.java) // ------------------------------------------------------------------------- // Methods dealing with com.datastax.oss.driver.api.core.cql.Statement @@ -79,54 +101,82 @@ inline fun AsyncCassandraOperations.selectOne(cql: String): Li /** * Extension for [AsyncCassandraOperations.select] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("select(statement)")) -fun AsyncCassandraOperations.select(statement: Statement<*>, entityClass: KClass): ListenableFuture> = - select(statement, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("select(statement)") +) +fun AsyncCassandraOperations.select( + statement: Statement<*>, + entityClass: KClass +): CompletableFuture> = + select(statement, entityClass.java) /** * Extension for [AsyncCassandraOperations.select] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.select(statement: Statement<*>): ListenableFuture> = - select(statement, T::class.java) +inline fun AsyncCassandraOperations.select(statement: Statement<*>): CompletableFuture> = + select(statement, T::class.java) /** * Extension for [AsyncCassandraOperations.select] providing a Consumer-like function. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("select(statement, consumer)")) -fun AsyncCassandraOperations.select(statement: Statement<*>, entityClass: KClass, consumer: (T) -> Unit): ListenableFuture = - select(statement, consumer, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("select(statement, consumer)") +) +fun AsyncCassandraOperations.select( + statement: Statement<*>, + entityClass: KClass, + consumer: (T) -> Unit +): CompletableFuture = + select(statement, consumer, entityClass.java) /** * Extension for [AsyncCassandraOperations.select] providing a Consumer-like function leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.select(statement: Statement<*>, crossinline consumer: (T) -> Unit): ListenableFuture = - select(statement, { consumer(it) }, T::class.java) +inline fun AsyncCassandraOperations.select( + statement: Statement<*>, + crossinline consumer: (T) -> Unit +): CompletableFuture = + select(statement, { consumer(it) }, T::class.java) /** * Extension for [AsyncCassandraOperations.slice] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("slice(statement)")) -fun AsyncCassandraOperations.slice(statement: Statement<*>, entityClass: KClass): ListenableFuture> = - slice(statement, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("slice(statement)") +) +fun AsyncCassandraOperations.slice( + statement: Statement<*>, + entityClass: KClass +): CompletableFuture> = + slice(statement, entityClass.java) /** * Extension for [AsyncCassandraOperations.slice] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.slice(statement: Statement<*>): ListenableFuture> = - slice(statement, T::class.java) +inline fun AsyncCassandraOperations.slice(statement: Statement<*>): CompletableFuture> = + slice(statement, T::class.java) /** * Extension for [AsyncCassandraOperations.selectOne] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("selectOne(statement)")) -fun AsyncCassandraOperations.selectOne(statement: Statement<*>, entityClass: KClass): ListenableFuture = - selectOne(statement, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("selectOne(statement)") +) +fun AsyncCassandraOperations.selectOne( + statement: Statement<*>, + entityClass: KClass +): CompletableFuture = + selectOne(statement, entityClass.java) /** * Extension for [AsyncCassandraOperations.selectOne] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.selectOne(statement: Statement<*>): ListenableFuture = - selectOne(statement, T::class.java) +inline fun AsyncCassandraOperations.selectOne(statement: Statement<*>): CompletableFuture = + selectOne(statement, T::class.java) // ------------------------------------------------------------------------- // Methods dealing with org.springframework.data.cassandra.core.query.Query @@ -135,82 +185,129 @@ inline fun AsyncCassandraOperations.selectOne(statement: State /** * Extension for [AsyncCassandraOperations.select] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("select(query)")) -fun AsyncCassandraOperations.select(query: Query, entityClass: KClass): ListenableFuture> = - select(query, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("select(query)") +) +fun AsyncCassandraOperations.select( + query: Query, + entityClass: KClass +): CompletableFuture> = + select(query, entityClass.java) /** * Extension for [AsyncCassandraOperations.select] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.select(query: Query): ListenableFuture> = - select(query, T::class.java) +inline fun AsyncCassandraOperations.select(query: Query): CompletableFuture> = + select(query, T::class.java) /** * Extension for [AsyncCassandraOperations.select] providing a Consumer-like function. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("select(query, consumer)")) -fun AsyncCassandraOperations.select(query: Query, entityClass: KClass, consumer: (T) -> Unit): ListenableFuture = - select(query, consumer, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("select(query, consumer)") +) +fun AsyncCassandraOperations.select( + query: Query, + entityClass: KClass, + consumer: (T) -> Unit +): CompletableFuture = + select(query, consumer, entityClass.java) /** * Extension for [AsyncCassandraOperations.select] providing a Consumer-like function leveraging reified type parameters. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("select(query, consumer)")) -inline fun AsyncCassandraOperations.select(query: Query, crossinline consumer: (T) -> Unit): ListenableFuture = - select(query, { consumer(it) }, T::class.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("select(query, consumer)") +) +inline fun AsyncCassandraOperations.select( + query: Query, + crossinline consumer: (T) -> Unit +): CompletableFuture = + select(query, { consumer(it) }, T::class.java) /** * Extension for [AsyncCassandraOperations.slice] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("slice(query)")) -fun AsyncCassandraOperations.slice(query: Query, entityClass: KClass): ListenableFuture> = - slice(query, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("slice(query)") +) +fun AsyncCassandraOperations.slice( + query: Query, + entityClass: KClass +): CompletableFuture> = + slice(query, entityClass.java) /** * Extension for [AsyncCassandraOperations.slice] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.slice(query: Query): ListenableFuture> = - slice(query, T::class.java) +inline fun AsyncCassandraOperations.slice(query: Query): CompletableFuture> = + slice(query, T::class.java) /** * Extension for [AsyncCassandraOperations.selectOne] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("selectOne(query)")) -fun AsyncCassandraOperations.selectOne(query: Query, entityClass: KClass): ListenableFuture = - selectOne(query, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("selectOne(query)") +) +fun AsyncCassandraOperations.selectOne( + query: Query, + entityClass: KClass +): CompletableFuture = + selectOne(query, entityClass.java) /** * Extension for [AsyncCassandraOperations.selectOne] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.selectOne(query: Query): ListenableFuture = - selectOne(query, T::class.java) +inline fun AsyncCassandraOperations.selectOne(query: Query): CompletableFuture = + selectOne(query, T::class.java) /** * Extension for [AsyncCassandraOperations.update] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("update(query, update)")) -fun AsyncCassandraOperations.update(query: Query, update: Update, entityClass: KClass): ListenableFuture = - update(query, update, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("update(query, update)") +) +fun AsyncCassandraOperations.update( + query: Query, + update: Update, + entityClass: KClass +): CompletableFuture = + update(query, update, entityClass.java) /** * Extension for [AsyncCassandraOperations.update] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.update(query: Query, update: Update): ListenableFuture = - update(query, update, T::class.java) +inline fun AsyncCassandraOperations.update( + query: Query, + update: Update +): CompletableFuture = + update(query, update, T::class.java) /** * Extension for [AsyncCassandraOperations.delete] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("delete(query)")) -fun AsyncCassandraOperations.delete(query: Query, entityClass: KClass): ListenableFuture = - delete(query, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("delete(query)") +) +fun AsyncCassandraOperations.delete( + query: Query, + entityClass: KClass +): CompletableFuture = + delete(query, entityClass.java) /** * Extension for [AsyncCassandraOperations.delete] leveraging reified type parameters. */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun AsyncCassandraOperations.delete(query: Query): ListenableFuture = - delete(query, T::class.java) +inline fun AsyncCassandraOperations.delete(query: Query): CompletableFuture = + delete(query, T::class.java) // ------------------------------------------------------------------------- // Methods dealing with entities @@ -220,89 +317,122 @@ inline fun AsyncCassandraOperations.delete(query: Query): List * Extension for [AsyncCassandraOperations.count] providing a [KClass] based variant. */ @Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("count()")) -fun AsyncCassandraOperations.count(entityClass: KClass): ListenableFuture = - count(entityClass.java) +fun AsyncCassandraOperations.count(entityClass: KClass): CompletableFuture = + count(entityClass.java) /** * Extension for [AsyncCassandraOperations.count] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.count(): ListenableFuture = - count(T::class.java) +inline fun AsyncCassandraOperations.count(): CompletableFuture = + count(T::class.java) /** * Extension for [AsyncCassandraOperations.count] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("count(query)")) -fun AsyncCassandraOperations.count(query: Query, entityClass: KClass): ListenableFuture = - count(query, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("count(query)") +) +fun AsyncCassandraOperations.count( + query: Query, + entityClass: KClass +): CompletableFuture = + count(query, entityClass.java) /** * Extension for [AsyncCassandraOperations.count] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.count(query: Query): ListenableFuture = - count(query, T::class.java) +inline fun AsyncCassandraOperations.count(query: Query): CompletableFuture = + count(query, T::class.java) /** * Extension for [AsyncCassandraOperations.exists] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("exists(id)")) -fun AsyncCassandraOperations.exists(id: Any, entityClass: KClass): ListenableFuture = - exists(id, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("exists(id)") +) +fun AsyncCassandraOperations.exists( + id: Any, + entityClass: KClass +): CompletableFuture = + exists(id, entityClass.java) /** * Extension for [AsyncCassandraOperations.exists] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.exists(id: Any): ListenableFuture = - exists(id, T::class.java) +inline fun AsyncCassandraOperations.exists(id: Any): CompletableFuture = + exists(id, T::class.java) /** * Extension for [AsyncCassandraOperations.count] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("exists(query)")) -fun AsyncCassandraOperations.exists(query: Query, entityClass: KClass): ListenableFuture = - exists(query, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("exists(query)") +) +fun AsyncCassandraOperations.exists( + query: Query, + entityClass: KClass +): CompletableFuture = + exists(query, entityClass.java) /** * Extension for [AsyncCassandraOperations.count] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.exists(query: Query): ListenableFuture = - exists(query, T::class.java) +inline fun AsyncCassandraOperations.exists(query: Query): CompletableFuture = + exists(query, T::class.java) /** * Extension for [AsyncCassandraOperations.selectOneById] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("selectOneById(id)")) -fun AsyncCassandraOperations.selectOneById(id: Any, entityClass: KClass): ListenableFuture = - selectOneById(id, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("selectOneById(id)") +) +fun AsyncCassandraOperations.selectOneById( + id: Any, + entityClass: KClass +): CompletableFuture = + selectOneById(id, entityClass.java) /** * Extension for [AsyncCassandraOperations.selectOneById] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.selectOneById(id: Any): ListenableFuture = - selectOneById(id, T::class.java) +inline fun AsyncCassandraOperations.selectOneById(id: Any): CompletableFuture = + selectOneById(id, T::class.java) /** * Extension for [AsyncCassandraOperations.deleteById] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("deleteById(id)")) -fun AsyncCassandraOperations.deleteById(id: Any, entityClass: KClass): ListenableFuture = - deleteById(id, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("deleteById(id)") +) +fun AsyncCassandraOperations.deleteById( + id: Any, + entityClass: KClass +): CompletableFuture = + deleteById(id, entityClass.java) /** * Extension for [AsyncCassandraOperations.deleteById] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.deleteById(id: Any): ListenableFuture = - deleteById(id, T::class.java) +inline fun AsyncCassandraOperations.deleteById(id: Any): CompletableFuture = + deleteById(id, T::class.java) /** * Extension for [AsyncCassandraOperations.truncate] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("truncate()")) -fun AsyncCassandraOperations.truncate(entityClass: KClass): ListenableFuture = - truncate(entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("truncate()") +) +fun AsyncCassandraOperations.truncate(entityClass: KClass): CompletableFuture = + truncate(entityClass.java) /** * Extension for [AsyncCassandraOperations.truncate] leveraging reified type parameters. */ -inline fun AsyncCassandraOperations.truncate(): ListenableFuture = - truncate(T::class.java) +inline fun AsyncCassandraOperations.truncate(): CompletableFuture = + truncate(T::class.java) diff --git a/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/cql/AsyncCqlOperationsExtensions.kt b/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/cql/AsyncCqlOperationsExtensions.kt index 3bf65fc85..5e8d68b5e 100644 --- a/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/cql/AsyncCqlOperationsExtensions.kt +++ b/spring-data-cassandra/src/main/kotlin/org/springframework/data/cassandra/core/cql/AsyncCqlOperationsExtensions.kt @@ -18,7 +18,7 @@ package org.springframework.data.cassandra.core.cql import com.datastax.oss.driver.api.core.cql.AsyncResultSet import com.datastax.oss.driver.api.core.cql.Row import com.datastax.oss.driver.api.core.cql.Statement -import org.springframework.util.concurrent.ListenableFuture +import java.util.concurrent.CompletableFuture import kotlin.reflect.KClass /** @@ -31,86 +31,129 @@ import kotlin.reflect.KClass /** * Extension for [AsyncCqlOperations.queryForObject] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("queryForObject(cql)")) -fun AsyncCqlOperations.queryForObject(cql: String, entityClass: KClass): ListenableFuture = - queryForObject(cql, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("queryForObject(cql)") +) +fun AsyncCqlOperations.queryForObject( + cql: String, + entityClass: KClass +): CompletableFuture = + queryForObject(cql, entityClass.java) /** * Extension for [AsyncCqlOperations.queryForObject] leveraging reified type parameters. */ -inline fun AsyncCqlOperations.queryForObject(cql: String): ListenableFuture = - queryForObject(cql, T::class.java) +inline fun AsyncCqlOperations.queryForObject(cql: String): CompletableFuture = + queryForObject(cql, T::class.java) /** * Extension for [AsyncCqlOperations.queryForObject] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("queryForObject(cql, args)")) -fun AsyncCqlOperations.queryForObject(cql: String, entityClass: KClass, vararg args: Any): ListenableFuture = - queryForObject(cql, entityClass.java, args) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("queryForObject(cql, args)") +) +fun AsyncCqlOperations.queryForObject( + cql: String, + entityClass: KClass, + vararg args: Any +): CompletableFuture = + queryForObject(cql, entityClass.java, args) /** * Extension for [AsyncCqlOperations.queryForObject] leveraging reified type parameters. */ -inline fun AsyncCqlOperations.queryForObject(cql: String, vararg args: Any): ListenableFuture = - queryForObject(cql, T::class.java, args) +inline fun AsyncCqlOperations.queryForObject( + cql: String, + vararg args: Any +): CompletableFuture = + queryForObject(cql, T::class.java, args) /** * Extension for [AsyncCqlOperations.queryForObject] leveraging reified type parameters. */ -fun AsyncCqlOperations.queryForObject(cql: String, vararg args: Any, function: (Row, Int) -> T): ListenableFuture = - queryForObject(cql, RowMapper { row, i -> function(row, i) }, *args) +fun AsyncCqlOperations.queryForObject( + cql: String, + vararg args: Any, + function: (Row, Int) -> T +): CompletableFuture = + queryForObject(cql, RowMapper { row, i -> function(row, i) }, *args) /** * Extension for [AsyncCqlOperations.queryForObject] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("queryForObject(statement)")) -fun AsyncCqlOperations.queryForObject(statement: Statement<*>, entityClass: KClass): ListenableFuture = - queryForObject(statement, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("queryForObject(statement)") +) +fun AsyncCqlOperations.queryForObject( + statement: Statement<*>, + entityClass: KClass +): CompletableFuture = + queryForObject(statement, entityClass.java) /** * Extension for [AsyncCqlOperations.queryForObject] leveraging reified type parameters. */ -inline fun AsyncCqlOperations.queryForObject(statement: Statement<*>): ListenableFuture = - queryForObject(statement, T::class.java) +inline fun AsyncCqlOperations.queryForObject(statement: Statement<*>): CompletableFuture = + queryForObject(statement, T::class.java) /** * Extension for [AsyncCqlOperations.queryForList] leveraging reified type parameters. */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun AsyncCqlOperations.queryForList(cql: String): ListenableFuture> = - queryForList(cql, T::class.java) +inline fun AsyncCqlOperations.queryForList(cql: String): CompletableFuture> = + queryForList(cql, T::class.java) /** * Extension for [AsyncCqlOperations.queryForList] leveraging reified type parameters. */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun AsyncCqlOperations.queryForList(cql: String, vararg args: Any): ListenableFuture> = - queryForList(cql, T::class.java, args) +inline fun AsyncCqlOperations.queryForList( + cql: String, + vararg args: Any +): CompletableFuture> = + queryForList(cql, T::class.java, args) /** * Extension for [AsyncCqlOperations.queryForList] providing a [KClass] based variant. */ -@Deprecated("Since 2.2, use the reified variant", replaceWith = ReplaceWith("queryForList(statement)")) -fun AsyncCqlOperations.queryForList(statement: Statement<*>, entityClass: KClass): ListenableFuture> = - queryForList(statement, entityClass.java) +@Deprecated( + "Since 2.2, use the reified variant", + replaceWith = ReplaceWith("queryForList(statement)") +) +fun AsyncCqlOperations.queryForList( + statement: Statement<*>, + entityClass: KClass +): CompletableFuture> = + queryForList(statement, entityClass.java) /** * Extension for [AsyncCqlOperations.queryForList] leveraging reified type parameters. */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun AsyncCqlOperations.queryForList(statement: Statement<*>): ListenableFuture> = - queryForList(statement, T::class.java) +inline fun AsyncCqlOperations.queryForList(statement: Statement<*>): CompletableFuture> = + queryForList(statement, T::class.java) /** * Extension for [AsyncCqlOperations.query] providing a ResultSetExtractor-like function * variant: `query("...", arg1, argN){ rs -> }`. */ -inline fun AsyncCqlOperations.query(cql: String, vararg args: Any, crossinline function: (AsyncResultSet) -> ListenableFuture): ListenableFuture = - query(cql, AsyncResultSetExtractor { function(it) }, *args) +inline fun AsyncCqlOperations.query( + cql: String, + vararg args: Any, + crossinline function: (AsyncResultSet) -> CompletableFuture +): CompletableFuture = + query(cql, AsyncResultSetExtractor { function(it) }, *args) /** * Extension for [AsyncCqlOperations.query] providing a RowMapper-like function * variant: `query("...", arg1, argN){ row, i -> }`. */ -fun AsyncCqlOperations.query(cql: String, vararg args: Any, function: (Row, Int) -> T): ListenableFuture> = - query(cql, RowMapper { row, i -> function(row, i) }, *args) +fun AsyncCqlOperations.query( + cql: String, + vararg args: Any, + function: (Row, Int) -> T +): CompletableFuture> = + query(cql, RowMapper { row, i -> function(row, i) }, *args) diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java index 5a40a14c1..bd19533eb 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateIntegrationTests.java @@ -38,7 +38,6 @@ import org.springframework.data.cassandra.repository.support.SchemaTestUtils; import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTests; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; -import org.springframework.util.concurrent.ListenableFuture; import com.datastax.oss.driver.api.core.uuid.Uuids; @@ -117,7 +116,7 @@ class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingInt assertThat(getUser(user.getId())).isNull(); - ListenableFuture insert = template.insert(user); + Future insert = template.insert(user); assertThat(getUninterruptibly(insert)).isEqualTo(user); assertThat(getUser(user.getId())).isEqualTo(user); @@ -130,7 +129,7 @@ class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingInt User user = new User("heisenberg", "Walter", "White"); - ListenableFuture> inserted = template.insert(user, lwtOptions); + Future> inserted = template.insert(user, lwtOptions); assertThat(getUninterruptibly(inserted).wasApplied()).isTrue(); } @@ -146,7 +145,7 @@ class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingInt user.setFirstname("Walter Hartwell"); - ListenableFuture> lwt = template.insert(user, lwtOptions); + Future> lwt = template.insert(user, lwtOptions); assertThat(getUninterruptibly(lwt).wasApplied()).isFalse(); assertThat(getUser(user.getId()).getFirstname()).isEqualTo("Walter"); @@ -159,7 +158,7 @@ class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingInt User result = getUninterruptibly(template.insert(user)); - ListenableFuture count = template.count(User.class); + Future count = template.count(User.class); assertThat(result).isSameAs(user); assertThat(getUninterruptibly(count)).isEqualTo(1L); } @@ -207,7 +206,7 @@ class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingInt User user = new User("heisenberg", "Walter", "White"); - ListenableFuture> lwt = template.update(user, lwtOptions); + Future> lwt = template.update(user, lwtOptions); assertThat(getUninterruptibly(lwt).wasApplied()).isFalse(); assertThat(getUser(user.getId())).isNull(); @@ -223,7 +222,7 @@ class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingInt user.setFirstname("Walter Hartwell"); - ListenableFuture> updated = template.update(user, lwtOptions); + Future> updated = template.update(user, lwtOptions); assertThat(getUninterruptibly(updated).wasApplied()).isTrue(); assertThat(getUninterruptibly(updated).getEntity()).isSameAs(user); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateUnitTests.java index 1bd12cc56..e9c2657e7 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/AsyncCassandraTemplateUnitTests.java @@ -46,7 +46,6 @@ import org.springframework.data.cassandra.core.query.Update; import org.springframework.data.cassandra.domain.User; import org.springframework.data.cassandra.domain.VersionedUser; import org.springframework.data.mapping.callback.EntityCallbacks; -import org.springframework.util.concurrent.ListenableFuture; import com.datastax.oss.driver.api.core.CqlIdentifier; import com.datastax.oss.driver.api.core.CqlSession; @@ -140,7 +139,7 @@ class AsyncCassandraTemplateUnitTests { when(row.getObject(1)).thenReturn("Walter"); when(row.getObject(2)).thenReturn("White"); - ListenableFuture> list = template.select("SELECT * FROM users", User.class); + CompletableFuture> list = template.select("SELECT * FROM users", User.class); assertThat(getUninterruptibly(list)).hasSize(1).contains(new User("myid", "Walter", "White")); verify(session).executeAsync(statementCaptor.capture()); @@ -165,7 +164,7 @@ class AsyncCassandraTemplateUnitTests { List list = new ArrayList<>(); - ListenableFuture result = template.select("SELECT * FROM users", list::add, User.class); + CompletableFuture result = template.select("SELECT * FROM users", list::add, User.class); assertThat(getUninterruptibly(result)).isNull(); assertThat(list).hasSize(1).contains(new User("myid", "Walter", "White")); @@ -178,7 +177,7 @@ class AsyncCassandraTemplateUnitTests { when(resultSet.currentPage()).thenThrow(new NoNodeAvailableException()); - ListenableFuture> list = template.select("SELECT * FROM users", User.class); + CompletableFuture> list = template.select("SELECT * FROM users", User.class); try { list.get(); @@ -207,7 +206,7 @@ class AsyncCassandraTemplateUnitTests { when(row.getObject(1)).thenReturn("Walter"); when(row.getObject(2)).thenReturn("White"); - ListenableFuture future = template.selectOne("SELECT * FROM users WHERE id='myid'", User.class); + CompletableFuture future = template.selectOne("SELECT * FROM users WHERE id='myid'", User.class); assertThat(getUninterruptibly(future)).isEqualTo(new User("myid", "Walter", "White")); verify(session).executeAsync(statementCaptor.capture()); @@ -230,7 +229,7 @@ class AsyncCassandraTemplateUnitTests { when(row.getObject(1)).thenReturn("Walter"); when(row.getObject(2)).thenReturn("White"); - ListenableFuture future = template.selectOneById("myid", User.class); + CompletableFuture future = template.selectOneById("myid", User.class); assertThat(getUninterruptibly(future)).isEqualTo(new User("myid", "Walter", "White")); verify(session).executeAsync(statementCaptor.capture()); @@ -242,7 +241,7 @@ class AsyncCassandraTemplateUnitTests { when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); - ListenableFuture future = template.selectOne("SELECT id FROM users WHERE id='myid'", String.class); + CompletableFuture future = template.selectOne("SELECT id FROM users WHERE id='myid'", String.class); assertThat(getUninterruptibly(future)).isNull(); } @@ -252,7 +251,7 @@ class AsyncCassandraTemplateUnitTests { when(resultSet.one()).thenReturn(row); - ListenableFuture future = template.exists("myid", User.class); + CompletableFuture future = template.exists("myid", User.class); assertThat(getUninterruptibly(future)).isTrue(); verify(session).executeAsync(statementCaptor.capture()); @@ -262,7 +261,7 @@ class AsyncCassandraTemplateUnitTests { @Test // DATACASS-292 void existsShouldReturnNonExistingElement() { - ListenableFuture future = template.exists("myid", User.class); + CompletableFuture future = template.exists("myid", User.class); assertThat(getUninterruptibly(future)).isFalse(); verify(session).executeAsync(statementCaptor.capture()); @@ -274,7 +273,7 @@ class AsyncCassandraTemplateUnitTests { when(resultSet.one()).thenReturn(row); - ListenableFuture future = template.exists(Query.empty(), User.class); + CompletableFuture future = template.exists(Query.empty(), User.class); assertThat(getUninterruptibly(future)).isTrue(); verify(session).executeAsync(statementCaptor.capture()); @@ -288,7 +287,7 @@ class AsyncCassandraTemplateUnitTests { when(row.getLong(0)).thenReturn(42L); when(columnDefinitions.size()).thenReturn(1); - ListenableFuture future = template.count(User.class); + CompletableFuture future = template.count(User.class); assertThat(getUninterruptibly(future)).isEqualTo(42L); verify(session).executeAsync(statementCaptor.capture()); @@ -302,7 +301,7 @@ class AsyncCassandraTemplateUnitTests { when(row.getLong(0)).thenReturn(42L); when(columnDefinitions.size()).thenReturn(1); - ListenableFuture future = template.count(Query.empty(), User.class); + CompletableFuture future = template.count(Query.empty(), User.class); assertThat(getUninterruptibly(future)).isEqualTo(42L); verify(session).executeAsync(statementCaptor.capture()); @@ -316,7 +315,7 @@ class AsyncCassandraTemplateUnitTests { User user = new User("heisenberg", "Walter", "White"); - ListenableFuture future = template.insert(user); + CompletableFuture future = template.insert(user); assertThat(getUninterruptibly(future)).isEqualTo(user); verify(session).executeAsync(statementCaptor.capture()); @@ -333,7 +332,7 @@ class AsyncCassandraTemplateUnitTests { VersionedUser user = new VersionedUser("heisenberg", "Walter", "White"); - ListenableFuture future = template.insert(user); + CompletableFuture future = template.insert(user); assertThat(getUninterruptibly(future)).isEqualTo(user); verify(session).executeAsync(statementCaptor.capture()); @@ -350,7 +349,7 @@ class AsyncCassandraTemplateUnitTests { when(session.executeAsync(any(Statement.class))) .thenReturn(TestResultSetFuture.failed(new NoNodeAvailableException())); - ListenableFuture future = template.insert(new User("heisenberg", "Walter", "White")); + CompletableFuture future = template.insert(new User("heisenberg", "Walter", "White")); try { future.get(); @@ -369,7 +368,7 @@ class AsyncCassandraTemplateUnitTests { User user = new User("heisenberg", "Walter", "White"); - ListenableFuture future = template.update(user); + CompletableFuture future = template.update(user); assertThat(getUninterruptibly(future)).isEqualTo(user); verify(session).executeAsync(statementCaptor.capture()); @@ -387,7 +386,7 @@ class AsyncCassandraTemplateUnitTests { VersionedUser user = new VersionedUser("heisenberg", "Walter", "White"); user.setVersion(0L); - ListenableFuture future = template.update(user); + CompletableFuture future = template.update(user); assertThat(getUninterruptibly(future)).isEqualTo(user); verify(session).executeAsync(statementCaptor.capture()); @@ -461,7 +460,7 @@ class AsyncCassandraTemplateUnitTests { when(session.executeAsync(any(Statement.class))) .thenReturn(TestResultSetFuture.failed(new NoNodeAvailableException())); - ListenableFuture future = template.update(new User("heisenberg", "Walter", "White")); + CompletableFuture future = template.update(new User("heisenberg", "Walter", "White")); try { future.get(); @@ -480,7 +479,7 @@ class AsyncCassandraTemplateUnitTests { User user = new User("heisenberg", "Walter", "White"); - ListenableFuture future = template.deleteById(user.getId(), User.class); + CompletableFuture future = template.deleteById(user.getId(), User.class); assertThat(getUninterruptibly(future)).isTrue(); verify(session).executeAsync(statementCaptor.capture()); @@ -494,7 +493,7 @@ class AsyncCassandraTemplateUnitTests { User user = new User("heisenberg", "Walter", "White"); - ListenableFuture future = template.delete(user); + CompletableFuture future = template.delete(user); assertThat(getUninterruptibly(future)).isEqualTo(user); verify(session).executeAsync(statementCaptor.capture()); @@ -534,7 +533,7 @@ class AsyncCassandraTemplateUnitTests { when(session.executeAsync(any(Statement.class))) .thenReturn(TestResultSetFuture.failed(new NoNodeAvailableException())); - ListenableFuture future = template.delete(new User("heisenberg", "Walter", "White")); + CompletableFuture future = template.delete(new User("heisenberg", "Walter", "White")); try { future.get(); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncCqlTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncCqlTemplateIntegrationTests.java index 5a7ea2e25..b6aa1c43e 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncCqlTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncCqlTemplateIntegrationTests.java @@ -27,7 +27,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTests; -import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import com.datastax.oss.driver.api.core.cql.SimpleStatement; @@ -150,8 +149,8 @@ class AsyncCqlTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrati void queryPreparedStatementCreatorShouldInvokeCallback() { List result = new ArrayList<>(); - getUninterruptibly(template.query(session -> new CompletableToListenableFutureAdapter<>( - session.prepareAsync("SELECT id FROM user WHERE id = ?;")), ps -> ps.bind("WHITE"), row -> { + getUninterruptibly(template.query(session -> session.prepareAsync("SELECT id FROM user WHERE id = ?;"), + ps -> ps.bind("WHITE"), row -> { result.add(row.getString(0)); })); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncCqlTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncCqlTemplateUnitTests.java index ee086e2ea..3023db395 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncCqlTemplateUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncCqlTemplateUnitTests.java @@ -42,8 +42,6 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.cassandra.CassandraConnectionFailureException; import org.springframework.data.cassandra.CassandraInvalidQueryException; import org.springframework.lang.Nullable; -import org.springframework.scheduling.annotation.AsyncResult; -import org.springframework.util.concurrent.ListenableFuture; import com.datastax.oss.driver.api.core.ConsistencyLevel; import com.datastax.oss.driver.api.core.CqlIdentifier; @@ -107,7 +105,7 @@ class AsyncCqlTemplateUnitTests { TestResultSetFuture resultSetFuture = TestResultSetFuture.failed(new NoNodeAvailableException()); when(session.executeAsync(any(Statement.class))).thenReturn(resultSetFuture); - ListenableFuture future = template.execute("UPDATE user SET a = 'b';"); + CompletableFuture future = template.execute("UPDATE user SET a = 'b';"); try { future.get(); @@ -203,8 +201,8 @@ class AsyncCqlTemplateUnitTests { TestResultSetFuture resultSetFuture = TestResultSetFuture.failed(new NoNodeAvailableException()); when(session.executeAsync(any(Statement.class))).thenReturn(resultSetFuture); - ListenableFuture future = template.query("UPDATE user SET a = 'b';", - (AsyncResultSetExtractor) it -> new AsyncResult<>(it.wasApplied())); + CompletableFuture future = template.query("UPDATE user SET a = 'b';", + (AsyncResultSetExtractor) it -> CompletableFuture.completedFuture(it.wasApplied())); try { future.get(); @@ -222,7 +220,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.emptyList()); - ListenableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> "OK"); + CompletableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> "OK"); try { future.get(); @@ -240,7 +238,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); - ListenableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> "OK"); + CompletableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> "OK"); assertThat(getUninterruptibly(future)).isEqualTo("OK"); } @@ -250,7 +248,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); - ListenableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> null); + CompletableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> null); assertThat(getUninterruptibly(future)).isNull(); } @@ -260,7 +258,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Arrays.asList(row, row)); - ListenableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> "OK"); + CompletableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> "OK"); try { future.get(); @@ -280,7 +278,7 @@ class AsyncCqlTemplateUnitTests { when(columnDefinitions.size()).thenReturn(1); when(row.getString(0)).thenReturn("OK"); - ListenableFuture future = template.queryForObject("SELECT * FROM user", String.class); + CompletableFuture future = template.queryForObject("SELECT * FROM user", String.class); assertThat(getUninterruptibly(future)).isEqualTo("OK"); } @@ -294,7 +292,7 @@ class AsyncCqlTemplateUnitTests { when(columnDefinitions.size()).thenReturn(1); when(row.getString(0)).thenReturn("OK", "NOT OK"); - ListenableFuture> future = template.queryForList("SELECT * FROM user", String.class); + CompletableFuture> future = template.queryForList("SELECT * FROM user", String.class); assertThat(getUninterruptibly(future)).contains("OK", "NOT OK"); } @@ -305,7 +303,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.wasApplied()).thenReturn(true); - ListenableFuture future = template.execute("UPDATE user SET a = 'b';"); + CompletableFuture future = template.execute("UPDATE user SET a = 'b';"); assertThat(getUninterruptibly(future)).isTrue(); } @@ -341,7 +339,7 @@ class AsyncCqlTemplateUnitTests { doTestStrings(asyncCqlTemplate -> { - ListenableFuture future = asyncCqlTemplate + CompletableFuture future = asyncCqlTemplate .queryForResultSet(SimpleStatement.newInstance("SELECT * from USERS")); assertThat(getUninterruptibly(future).currentPage()).hasSize(3); @@ -354,8 +352,8 @@ class AsyncCqlTemplateUnitTests { doTestStrings(asyncCqlTemplate -> { - ListenableFuture> future = asyncCqlTemplate.query(SimpleStatement.newInstance("SELECT * from USERS"), - (row, index) -> row.getString(0)); + CompletableFuture> future = asyncCqlTemplate + .query(SimpleStatement.newInstance("SELECT * from USERS"), (row, index) -> row.getString(0)); assertThat(getUninterruptibly(future)).hasSize(3).contains("Walter", "Hank", "Jesse"); verify(session).executeAsync(any(Statement.class)); @@ -367,8 +365,8 @@ class AsyncCqlTemplateUnitTests { doTestStrings(5, ConsistencyLevel.ONE, null, asyncCqlTemplate -> { - ListenableFuture> future = asyncCqlTemplate.query(SimpleStatement.newInstance("SELECT * from USERS"), - (row, index) -> row.getString(0)); + CompletableFuture> future = asyncCqlTemplate + .query(SimpleStatement.newInstance("SELECT * from USERS"), (row, index) -> row.getString(0)); assertThat(getUninterruptibly(future)).hasSize(3).contains("Walter", "Hank", "Jesse"); verify(session).executeAsync(any(Statement.class)); @@ -381,8 +379,8 @@ class AsyncCqlTemplateUnitTests { TestResultSetFuture resultSetFuture = TestResultSetFuture.failed(new NoNodeAvailableException()); when(session.executeAsync(any(Statement.class))).thenReturn(resultSetFuture); - ListenableFuture future = template.query(SimpleStatement.newInstance("UPDATE user SET a = 'b';"), - (AsyncResultSetExtractor) rs -> new AsyncResult<>(rs.wasApplied())); + CompletableFuture future = template.query(SimpleStatement.newInstance("UPDATE user SET a = 'b';"), + (AsyncResultSetExtractor) rs -> CompletableFuture.completedFuture(rs.wasApplied())); try { future.get(); @@ -400,7 +398,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.emptyList()); - ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + CompletableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), (row, rowNum) -> "OK"); try { @@ -419,7 +417,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); - ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + CompletableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), (row, rowNum) -> "OK"); assertThat(getUninterruptibly(future)).isEqualTo("OK"); } @@ -430,7 +428,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); - ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + CompletableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), (row, rowNum) -> null); assertThat(getUninterruptibly(future)).isNull(); } @@ -441,7 +439,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Arrays.asList(row, row)); - ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + CompletableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), (row, rowNum) -> "OK"); try { future.get(); @@ -462,7 +460,7 @@ class AsyncCqlTemplateUnitTests { when(columnDefinitions.size()).thenReturn(1); when(row.getString(0)).thenReturn("OK"); - ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + CompletableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), String.class); assertThat(getUninterruptibly(future)).isEqualTo("OK"); @@ -477,7 +475,7 @@ class AsyncCqlTemplateUnitTests { when(columnDefinitions.size()).thenReturn(1); when(row.getString(0)).thenReturn("OK", "NOT OK"); - ListenableFuture> future = template.queryForList(SimpleStatement.newInstance("SELECT * FROM user"), + CompletableFuture> future = template.queryForList(SimpleStatement.newInstance("SELECT * FROM user"), String.class); assertThat(getUninterruptibly(future)).contains("OK", "NOT OK"); @@ -489,7 +487,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.wasApplied()).thenReturn(true); - ListenableFuture future = template.execute(SimpleStatement.newInstance("UPDATE user SET a = 'b';")); + CompletableFuture future = template.execute(SimpleStatement.newInstance("UPDATE user SET a = 'b';")); assertThat(getUninterruptibly(future)).isTrue(); } @@ -503,8 +501,8 @@ class AsyncCqlTemplateUnitTests { doTestStrings(asyncCqlTemplate -> { - ListenableFuture> futureOfFuture = asyncCqlTemplate.execute("SELECT * from USERS", - (session, ps) -> session.executeAsync(ps.bind("A"))); + CompletableFuture> futureOfFuture = asyncCqlTemplate + .execute("SELECT * from USERS", (session, ps) -> session.executeAsync(ps.bind("A"))); try { assertThat(getUninterruptibly(futureOfFuture).toCompletableFuture().get().currentPage()).hasSize(3); @@ -522,7 +520,7 @@ class AsyncCqlTemplateUnitTests { when(this.preparedStatement.bind("White")).thenReturn(this.boundStatement); when(this.resultSet.wasApplied()).thenReturn(true); - ListenableFuture applied = asyncCqlTemplate.execute("UPDATE users SET name = ?", "White"); + CompletableFuture applied = asyncCqlTemplate.execute("UPDATE users SET name = ?", "White"); assertThat(getUninterruptibly(applied)).isTrue(); }); @@ -540,8 +538,8 @@ class AsyncCqlTemplateUnitTests { assertThat(e).hasMessageContaining("No node was available"); } - ListenableFuture> future = template.execute( - session -> AsyncResult.forExecutionException(new NoNodeAvailableException()), + CompletableFuture> future = template.execute( + session -> CompletableFuture.failedFuture(new NoNodeAvailableException()), (session, ps) -> session.executeAsync(boundStatement)); try { @@ -557,9 +555,10 @@ class AsyncCqlTemplateUnitTests { @Test // DATACASS-292 void executePreparedStatementCreatorShouldTranslateStatementCallbackExceptions() throws Exception { - ListenableFuture future = template.execute(session -> new AsyncResult<>(preparedStatement), (session, ps) -> { - throw new NoNodeAvailableException(); - }); + CompletableFuture future = template.execute(session -> CompletableFuture.completedFuture(preparedStatement), + (session, ps) -> { + throw new NoNodeAvailableException(); + }); try { future.get(); @@ -578,8 +577,9 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); - ListenableFuture> future = template.query(session -> new AsyncResult<>(preparedStatement), - (AsyncResultSetExtractor>) rs -> new AsyncResult<>(rs.currentPage())); + CompletableFuture> future = template.query( + session -> CompletableFuture.completedFuture(preparedStatement), + (AsyncResultSetExtractor>) rs -> CompletableFuture.completedFuture(rs.currentPage())); assertThat(getUninterruptibly(future)).contains(row); verify(preparedStatement).bind(); @@ -591,11 +591,11 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); - ListenableFuture future = template - .query(session -> new AsyncResult(preparedStatement), ps -> { + CompletableFuture future = template + .query(session -> CompletableFuture.completedFuture(preparedStatement), ps -> { ps.bind("a", "b"); return boundStatement; - }, (AsyncResultSetExtractor) AsyncResult::new); + }, (AsyncResultSetExtractor) CompletableFuture::completedFuture); assertThat(getUninterruptibly(future).currentPage()).contains(row); verify(preparedStatement).bind("a", "b"); @@ -604,11 +604,11 @@ class AsyncCqlTemplateUnitTests { @Test // DATACASS-292 void queryPreparedStatementCreatorAndBinderShouldTranslatePrepareStatementExceptions() throws Exception { - ListenableFuture future = template - .query(session -> AsyncResult.forExecutionException(new NoNodeAvailableException()), ps -> { + CompletableFuture future = template + .query(session -> CompletableFuture.failedFuture(new NoNodeAvailableException()), ps -> { ps.bind("a", "b"); return boundStatement; - }, (AsyncResultSetExtractor) AsyncResult::new); + }, (AsyncResultSetExtractor) CompletableFuture::completedFuture); try { future.get(); @@ -622,9 +622,10 @@ class AsyncCqlTemplateUnitTests { @Test // DATACASS-292 void queryPreparedStatementCreatorAndBinderShouldTranslateBindExceptions() throws Exception { - ListenableFuture future = template.query(session -> new AsyncResult<>(preparedStatement), ps -> { - throw new NoNodeAvailableException(); - }, (AsyncResultSetExtractor) AsyncResult::new); + CompletableFuture future = template + .query(session -> CompletableFuture.completedFuture(preparedStatement), ps -> { + throw new NoNodeAvailableException(); + }, (AsyncResultSetExtractor) CompletableFuture::completedFuture); try { future.get(); @@ -641,10 +642,11 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(boundStatement)).thenReturn(resultSetFuture); - ListenableFuture future = template.query(session -> new AsyncResult<>(preparedStatement), ps -> { - ps.bind("a", "b"); - return boundStatement; - }, (AsyncResultSetExtractor) AsyncResult::new); + CompletableFuture future = template + .query(session -> CompletableFuture.completedFuture(preparedStatement), ps -> { + ps.bind("a", "b"); + return boundStatement; + }, (AsyncResultSetExtractor) CompletableFuture::completedFuture); try { future.get(); @@ -660,10 +662,11 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); - ListenableFuture> future = template.query(session -> new AsyncResult<>(preparedStatement), ps -> { - ps.bind("a", "b"); - return boundStatement; - }, (row, rowNum) -> row); + CompletableFuture> future = template + .query(session -> CompletableFuture.completedFuture(preparedStatement), ps -> { + ps.bind("a", "b"); + return boundStatement; + }, (row, rowNum) -> row); assertThat(getUninterruptibly(future)).hasSize(1).contains(row); verify(preparedStatement).bind("a", "b"); @@ -678,7 +681,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.emptyList()); - ListenableFuture future = template.queryForObject("SELECT * FROM user WHERE username = ?", + CompletableFuture future = template.queryForObject("SELECT * FROM user WHERE username = ?", (row, rowNum) -> "OK", "Walter"); try { @@ -700,7 +703,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); - ListenableFuture future = template.queryForObject("SELECT * FROM user WHERE username = ?", + CompletableFuture future = template.queryForObject("SELECT * FROM user WHERE username = ?", (row, rowNum) -> "OK", "Walter"); assertThat(getUninterruptibly(future)).isEqualTo("OK"); } @@ -714,7 +717,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.currentPage()).thenReturn(Arrays.asList(row, row)); - ListenableFuture future = template.queryForObject("SELECT * FROM user WHERE username = ?", + CompletableFuture future = template.queryForObject("SELECT * FROM user WHERE username = ?", (row, rowNum) -> "OK", "Walter"); try { future.get(); @@ -755,8 +758,8 @@ class AsyncCqlTemplateUnitTests { when(columnDefinitions.size()).thenReturn(1); when(row.getString(0)).thenReturn("OK", "NOT OK"); - ListenableFuture> future = template.queryForList("SELECT * FROM user WHERE username = ?", String.class, - "Walter"); + CompletableFuture> future = template.queryForList("SELECT * FROM user WHERE username = ?", + String.class, "Walter"); assertThat(getUninterruptibly(future)).contains("OK", "NOT OK"); } @@ -770,7 +773,7 @@ class AsyncCqlTemplateUnitTests { when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); when(resultSet.wasApplied()).thenReturn(true); - ListenableFuture future = template.execute("UPDATE user SET username = ?", "Walter"); + CompletableFuture future = template.execute("UPDATE user SET username = ?", "Walter"); assertThat(getUninterruptibly(future)).isTrue(); } @@ -780,8 +783,7 @@ class AsyncCqlTemplateUnitTests { } private void doTestStrings(@Nullable Integer fetchSize, @Nullable ConsistencyLevel consistencyLevel, - @Nullable CqlIdentifier keyspace, - Consumer cqlTemplateConsumer) { + @Nullable CqlIdentifier keyspace, Consumer cqlTemplateConsumer) { String[] results = { "Walter", "Hank", "Jesse" }; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncResultStreamUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncResultStreamUnitTests.java index 8e32c9b93..090be2078 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncResultStreamUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/AsyncResultStreamUnitTests.java @@ -27,8 +27,6 @@ import java.util.stream.Collectors; import org.junit.jupiter.api.Test; -import org.springframework.util.concurrent.ListenableFuture; - import com.datastax.oss.driver.api.core.cql.AsyncResultSet; import com.datastax.oss.driver.api.core.cql.Row; @@ -95,7 +93,7 @@ class AsyncResultStreamUnitTests { List rows = new ArrayList<>(); - ListenableFuture completion = AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum) + CompletableFuture completion = AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum) .forEach(rows::add); assertThatThrownBy(completion::get).hasRootCauseInstanceOf(RuntimeException.class); @@ -106,7 +104,7 @@ class AsyncResultStreamUnitTests { when(first.currentPage()).thenReturn(Collections.singletonList(row1)); - ListenableFuture> collect = AsyncResultStream.from(first).collect(Collectors.toList()); + CompletableFuture> collect = AsyncResultStream.from(first).collect(Collectors.toList()); assertThat(collect.get()).containsOnly(row1); } @@ -119,7 +117,7 @@ class AsyncResultStreamUnitTests { when(first.fetchNextPage()).thenReturn(CompletableFuture.completedFuture(last)); when(first.hasMorePages()).thenReturn(true); - ListenableFuture> rows = AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum) + CompletableFuture> rows = AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum) .collect(Collectors.toList()); assertThat(rows.get()).containsOnly("row-1", "row-2"); @@ -135,7 +133,7 @@ class AsyncResultStreamUnitTests { when(first.fetchNextPage()).thenReturn(failed); when(first.hasMorePages()).thenReturn(true); - ListenableFuture> collect = AsyncResultStream.from(first).collect(Collectors.toList()); + CompletableFuture> collect = AsyncResultStream.from(first).collect(Collectors.toList()); assertThatThrownBy(collect::get).hasRootCauseInstanceOf(RuntimeException.class); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/legacy/AsyncCqlTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/legacy/AsyncCqlTemplateIntegrationTests.java new file mode 100644 index 000000000..60d983705 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/legacy/AsyncCqlTemplateIntegrationTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2016-2022 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 + * + * https://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.cql.legacy; + +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTests; +import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; + +import com.datastax.oss.driver.api.core.cql.SimpleStatement; + +/** + * Integration tests for {@link AsyncCqlTemplate}. + * + * @author Mark Paluch + */ +class AsyncCqlTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTests { + + private static final AtomicBoolean initialized = new AtomicBoolean(); + private AsyncCqlTemplate template; + + @BeforeEach + void before() { + + if (initialized.compareAndSet(false, true)) { + session.execute("CREATE TABLE IF NOT EXISTS user (id text PRIMARY KEY, username text);"); + } + + session.execute("TRUNCATE user;"); + session.execute("INSERT INTO user (id, username) VALUES ('WHITE', 'Walter');"); + + template = new AsyncCqlTemplate(); + template.setSession(getSession()); + } + + @Test // DATACASS-292 + void executeShouldRemoveRecords() { + + getUninterruptibly(template.execute("DELETE FROM user WHERE id = 'WHITE'")); + + assertThat(session.execute("SELECT * FROM user").one()).isNull(); + } + + @Test // DATACASS-292 + void queryShouldInvokeCallback() { + + List result = new ArrayList<>(); + getUninterruptibly(template.query("SELECT id FROM user;", row -> { + result.add(row.getString(0)); + })); + + assertThat(result).contains("WHITE"); + } + + @Test // DATACASS-292 + void queryForObjectShouldReturnFirstColumn() { + + String id = getUninterruptibly(template.queryForObject("SELECT id FROM user;", String.class)); + + assertThat(id).isEqualTo("WHITE"); + } + + @Test // DATACASS-292 + void queryForObjectShouldReturnMap() { + + Map map = getUninterruptibly(template.queryForMap("SELECT * FROM user;")); + + assertThat(map).containsEntry("id", "WHITE").containsEntry("username", "Walter"); + } + + @Test // DATACASS-292 + void executeStatementShouldRemoveRecords() { + + getUninterruptibly(template.execute(SimpleStatement.newInstance("DELETE FROM user WHERE id = 'WHITE'"))); + + assertThat(session.execute("SELECT * FROM user").one()).isNull(); + } + + @Test // DATACASS-292 + void queryStatementShouldInvokeCallback() { + + List result = new ArrayList<>(); + getUninterruptibly(template.query(SimpleStatement.newInstance("SELECT id FROM user"), row -> { + result.add(row.getString(0)); + })); + + assertThat(result).contains("WHITE"); + } + + @Test // DATACASS-292 + void queryForObjectStatementShouldReturnFirstColumn() { + + String id = getUninterruptibly( + template.queryForObject(SimpleStatement.newInstance("SELECT id FROM user"), String.class)); + + assertThat(id).isEqualTo("WHITE"); + } + + @Test // DATACASS-292 + void queryForObjectStatementShouldReturnMap() { + + Map map = getUninterruptibly( + template.queryForMap(SimpleStatement.newInstance("SELECT * FROM user"))); + + assertThat(map).containsEntry("id", "WHITE").containsEntry("username", "Walter"); + } + + @Test // DATACASS-292 + void executeWithArgsShouldRemoveRecords() { + + getUninterruptibly(template.execute("DELETE FROM user WHERE id = ?", "WHITE")); + + assertThat(session.execute("SELECT * FROM user").one()).isNull(); + } + + @Test // DATACASS-292 + void queryPreparedStatementShouldInvokeCallback() { + + List result = new ArrayList<>(); + getUninterruptibly(template.query("SELECT id FROM user WHERE id = ?;", row -> { + result.add(row.getString(0)); + }, "WHITE")); + + assertThat(result).contains("WHITE"); + } + + @Test // DATACASS-292 + void queryPreparedStatementCreatorShouldInvokeCallback() { + + List result = new ArrayList<>(); + getUninterruptibly(template.query(session -> new CompletableToListenableFutureAdapter<>( + session.prepareAsync("SELECT id FROM user WHERE id = ?;")), ps -> ps.bind("WHITE"), row -> { + result.add(row.getString(0)); + })); + + assertThat(result).contains("WHITE"); + } + + @Test // DATACASS-292 + void queryForObjectWithArgsShouldReturnFirstColumn() { + + String id = getUninterruptibly(template.queryForObject("SELECT id FROM user WHERE id = ?;", String.class, "WHITE")); + + assertThat(id).isEqualTo("WHITE"); + } + + @Test // DATACASS-292 + void queryForObjectWithArgsShouldReturnMap() { + + Map map = getUninterruptibly(template.queryForMap("SELECT * FROM user WHERE id = ?;", "WHITE")); + + assertThat(map).containsEntry("id", "WHITE").containsEntry("username", "Walter"); + } + + private static T getUninterruptibly(Future future) { + + try { + return future.get(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/legacy/AsyncCqlTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/legacy/AsyncCqlTemplateUnitTests.java new file mode 100644 index 000000000..30c1c5bc3 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/legacy/AsyncCqlTemplateUnitTests.java @@ -0,0 +1,865 @@ +/* + * Copyright 2016-2022 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 + * + * https://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.cql.legacy; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.cassandra.CassandraConnectionFailureException; +import org.springframework.data.cassandra.CassandraInvalidQueryException; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.annotation.AsyncResult; +import org.springframework.util.concurrent.ListenableFuture; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.NoNodeAvailableException; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.servererrors.InvalidQueryException; + +/** + * Unit tests for {@link AsyncCqlTemplate}. + * + * @author Mark Paluch + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class AsyncCqlTemplateUnitTests { + + @Mock CqlSession session; + @Mock AsyncResultSet resultSet; + @Mock Row row; + @Mock PreparedStatement preparedStatement; + @Mock BoundStatement boundStatement; + @Mock ColumnDefinitions columnDefinitions; + + private AsyncCqlTemplate template; + + @BeforeEach + void setup() { + + this.template = new AsyncCqlTemplate(); + this.template.setSession(session); + } + + // ------------------------------------------------------------------------- + // Tests dealing with a plain com.datastax.oss.driver.api.core.CqlSession + // ------------------------------------------------------------------------- + + @Test // DATACASS-292 + void executeCallbackShouldTranslateExceptions() { + + try { + template.execute((AsyncSessionCallback) session -> { + throw new InvalidQueryException(null, "wrong query"); + }); + + fail("Missing CassandraInvalidQueryException"); + } catch (CassandraInvalidQueryException e) { + assertThat(e).hasMessageContaining("wrong query"); + } + } + + @Test // DATACASS-292 + void executeCqlShouldTranslateExceptions() throws Exception { + + TestResultSetFuture resultSetFuture = TestResultSetFuture.failed(new NoNodeAvailableException()); + when(session.executeAsync(any(Statement.class))).thenReturn(resultSetFuture); + + ListenableFuture future = template.execute("UPDATE user SET a = 'b';"); + + try { + future.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class) + .hasMessageContaining("No node was available"); + } + } + + // ------------------------------------------------------------------------- + // Tests dealing with static CQL + // ------------------------------------------------------------------------- + + @Test // DATACASS-292 + void executeCqlShouldCallExecution() { + + doTestStrings(asyncCqlTemplate -> { + + asyncCqlTemplate.execute("SELECT * from USERS"); + + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-767 + void executePreparedStatementShouldApplyKeyspace() { + + doTestStrings(null, null, CqlIdentifier.fromCql("ks1"), cqlTemplate -> { + cqlTemplate.execute("SELECT * from USERS", (session, ps) -> session.executeAsync(ps.bind("A"))); + }); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SimpleStatement.class); + verify(session).prepareAsync(captor.capture()); + + SimpleStatement statement = captor.getValue(); + assertThat(statement.getKeyspace()).isEqualTo(CqlIdentifier.fromCql("ks1")); + } + + @Test // DATACASS-292 + void executeCqlWithArgumentsShouldCallExecution() { + + doTestStrings(5, ConsistencyLevel.ONE, null, asyncCqlTemplate -> { + + asyncCqlTemplate.execute("SELECT * from USERS"); + + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-292 + void queryForResultSetShouldCallExecution() { + + doTestStrings(asyncCqlTemplate -> { + + AsyncResultSet resultSet = getUninterruptibly(asyncCqlTemplate.queryForResultSet("SELECT * from USERS")); + + assertThat(resultSet.currentPage()).hasSize(3); + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-292 + void queryWithResultSetExtractorShouldCallExecution() { + + doTestStrings(asyncCqlTemplate -> { + + List rows = getUninterruptibly( + asyncCqlTemplate.query("SELECT * from USERS", (row, index) -> row.getString(0))); + + assertThat(rows).hasSize(3).contains("Walter", "Hank", "Jesse"); + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-292 + void queryWithResultSetExtractorWithArgumentsShouldCallExecution() { + + doTestStrings(5, ConsistencyLevel.ONE, null, asyncCqlTemplate -> { + + List rows = getUninterruptibly( + asyncCqlTemplate.query("SELECT * from USERS", (row, index) -> row.getString(0))); + + assertThat(rows).hasSize(3).contains("Walter", "Hank", "Jesse"); + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-292 + void queryCqlShouldTranslateExceptions() throws Exception { + + TestResultSetFuture resultSetFuture = TestResultSetFuture.failed(new NoNodeAvailableException()); + when(session.executeAsync(any(Statement.class))).thenReturn(resultSetFuture); + + ListenableFuture future = template.query("UPDATE user SET a = 'b';", + (AsyncResultSetExtractor) it -> new AsyncResult<>(it.wasApplied())); + + try { + future.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class) + .hasMessageContaining("No node was available"); + } + } + + @Test // DATACASS-292 + void queryForObjectCqlShouldBeEmpty() throws Exception { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.emptyList()); + + ListenableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> "OK"); + + try { + future.get(); + + fail("Missing IncorrectResultSizeDataAccessException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(EmptyResultDataAccessException.class) + .hasMessageContaining("expected 1, actual 0"); + } + } + + @Test // DATACASS-292 + void queryForObjectCqlShouldReturnRecord() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + + ListenableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> "OK"); + assertThat(getUninterruptibly(future)).isEqualTo("OK"); + } + + @Test // DATACASS-292 + void queryForObjectCqlShouldReturnNullValue() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + + ListenableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> null); + assertThat(getUninterruptibly(future)).isNull(); + } + + @Test // DATACASS-292 + void queryForObjectCqlShouldFailReturningManyRecords() throws Exception { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Arrays.asList(row, row)); + + ListenableFuture future = template.queryForObject("SELECT * FROM user", (row, rowNum) -> "OK"); + try { + future.get(); + + fail("Missing IncorrectResultSizeDataAccessException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(IncorrectResultSizeDataAccessException.class) + .hasMessageContaining("expected 1, actual 2"); + } + } + + @Test // DATACASS-292 + void queryForObjectCqlWithTypeShouldReturnRecord() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + when(row.getColumnDefinitions()).thenReturn(columnDefinitions); + when(columnDefinitions.size()).thenReturn(1); + when(row.getString(0)).thenReturn("OK"); + + ListenableFuture future = template.queryForObject("SELECT * FROM user", String.class); + + assertThat(getUninterruptibly(future)).isEqualTo("OK"); + } + + @Test // DATACASS-292 + void queryForListCqlWithTypeShouldReturnRecord() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Arrays.asList(row, row)); + when(row.getColumnDefinitions()).thenReturn(columnDefinitions); + when(columnDefinitions.size()).thenReturn(1); + when(row.getString(0)).thenReturn("OK", "NOT OK"); + + ListenableFuture> future = template.queryForList("SELECT * FROM user", String.class); + + assertThat(getUninterruptibly(future)).contains("OK", "NOT OK"); + } + + @Test // DATACASS-292 + void executeCqlShouldReturnWasApplied() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.wasApplied()).thenReturn(true); + + ListenableFuture future = template.execute("UPDATE user SET a = 'b';"); + + assertThat(getUninterruptibly(future)).isTrue(); + } + + // ------------------------------------------------------------------------- + // Tests dealing with com.datastax.oss.driver.api.core.cql.Statement + // ------------------------------------------------------------------------- + + @Test // DATACASS-292 + void executeStatementShouldCallExecution() { + + doTestStrings(asyncCqlTemplate -> { + + asyncCqlTemplate.execute(SimpleStatement.newInstance("SELECT * from USERS")); + + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-292 + void executeStatementWithArgumentsShouldCallExecution() { + + doTestStrings(5, ConsistencyLevel.ONE, null, asyncCqlTemplate -> { + + asyncCqlTemplate.execute(SimpleStatement.newInstance("SELECT * from USERS")); + + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-292 + void queryForResultStatementSetShouldCallExecution() { + + doTestStrings(asyncCqlTemplate -> { + + ListenableFuture future = asyncCqlTemplate + .queryForResultSet(SimpleStatement.newInstance("SELECT * from USERS")); + + assertThat(getUninterruptibly(future).currentPage()).hasSize(3); + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-292 + void queryWithResultSetStatementExtractorShouldCallExecution() { + + doTestStrings(asyncCqlTemplate -> { + + ListenableFuture> future = asyncCqlTemplate.query(SimpleStatement.newInstance("SELECT * from USERS"), + (row, index) -> row.getString(0)); + + assertThat(getUninterruptibly(future)).hasSize(3).contains("Walter", "Hank", "Jesse"); + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-292 + void queryWithResultSetStatementExtractorWithArgumentsShouldCallExecution() { + + doTestStrings(5, ConsistencyLevel.ONE, null, asyncCqlTemplate -> { + + ListenableFuture> future = asyncCqlTemplate.query(SimpleStatement.newInstance("SELECT * from USERS"), + (row, index) -> row.getString(0)); + + assertThat(getUninterruptibly(future)).hasSize(3).contains("Walter", "Hank", "Jesse"); + verify(session).executeAsync(any(Statement.class)); + }); + } + + @Test // DATACASS-292 + void queryStatementShouldTranslateExceptions() throws Exception { + + TestResultSetFuture resultSetFuture = TestResultSetFuture.failed(new NoNodeAvailableException()); + when(session.executeAsync(any(Statement.class))).thenReturn(resultSetFuture); + + ListenableFuture future = template.query(SimpleStatement.newInstance("UPDATE user SET a = 'b';"), + (AsyncResultSetExtractor) rs -> new AsyncResult<>(rs.wasApplied())); + + try { + future.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class) + .hasMessageContaining("No node was available"); + } + } + + @Test // DATACASS-292 + void queryForObjectStatementShouldBeEmpty() throws Exception { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.emptyList()); + + ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + (row, rowNum) -> "OK"); + + try { + future.get(); + + fail("Missing IncorrectResultSizeDataAccessException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(EmptyResultDataAccessException.class) + .hasMessageContaining("expected 1, actual 0"); + } + } + + @Test // DATACASS-292 + void queryForObjectStatementShouldReturnRecord() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + + ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + (row, rowNum) -> "OK"); + assertThat(getUninterruptibly(future)).isEqualTo("OK"); + } + + @Test // DATACASS-292 + void queryForObjectStatementShouldReturnNullValue() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + + ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + (row, rowNum) -> null); + assertThat(getUninterruptibly(future)).isNull(); + } + + @Test // DATACASS-292 + void queryForObjectStatementShouldFailReturningManyRecords() throws Exception { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Arrays.asList(row, row)); + + ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + (row, rowNum) -> "OK"); + try { + future.get(); + + fail("Missing IncorrectResultSizeDataAccessException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(IncorrectResultSizeDataAccessException.class) + .hasMessageContaining("expected 1, actual 2"); + } + } + + @Test // DATACASS-292 + void queryForObjectStatementWithTypeShouldReturnRecord() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + when(row.getColumnDefinitions()).thenReturn(columnDefinitions); + when(columnDefinitions.size()).thenReturn(1); + when(row.getString(0)).thenReturn("OK"); + + ListenableFuture future = template.queryForObject(SimpleStatement.newInstance("SELECT * FROM user"), + String.class); + + assertThat(getUninterruptibly(future)).isEqualTo("OK"); + } + + @Test // DATACASS-292 + void queryForListStatementWithTypeShouldReturnRecord() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Arrays.asList(row, row)); + when(row.getColumnDefinitions()).thenReturn(columnDefinitions); + when(columnDefinitions.size()).thenReturn(1); + when(row.getString(0)).thenReturn("OK", "NOT OK"); + + ListenableFuture> future = template.queryForList(SimpleStatement.newInstance("SELECT * FROM user"), + String.class); + + assertThat(getUninterruptibly(future)).contains("OK", "NOT OK"); + } + + @Test // DATACASS-292 + void executeStatementShouldReturnWasApplied() { + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.wasApplied()).thenReturn(true); + + ListenableFuture future = template.execute(SimpleStatement.newInstance("UPDATE user SET a = 'b';")); + + assertThat(getUninterruptibly(future)).isTrue(); + } + + // ------------------------------------------------------------------------- + // Methods dealing with prepared statements + // ------------------------------------------------------------------------- + + @Test // DATACASS-292 + void queryPreparedStatementWithCallbackShouldCallExecution() { + + doTestStrings(asyncCqlTemplate -> { + + ListenableFuture> futureOfFuture = asyncCqlTemplate.execute("SELECT * from USERS", + (session, ps) -> session.executeAsync(ps.bind("A"))); + + try { + assertThat(getUninterruptibly(futureOfFuture).toCompletableFuture().get().currentPage()).hasSize(3); + } catch (Exception e) { + fail(e.getMessage(), e); + } + }); + } + + @Test // DATACASS-292 + void executePreparedStatementWithCallbackShouldCallExecution() { + + doTestStrings(asyncCqlTemplate -> { + + when(this.preparedStatement.bind("White")).thenReturn(this.boundStatement); + when(this.resultSet.wasApplied()).thenReturn(true); + + ListenableFuture applied = asyncCqlTemplate.execute("UPDATE users SET name = ?", "White"); + + assertThat(getUninterruptibly(applied)).isTrue(); + }); + } + + @Test // DATACASS-292 + void executePreparedStatementCreatorShouldTranslateStatementCreationExceptions() throws Exception { + + try { + template.execute(session -> { + throw new NoNodeAvailableException(); + }, (session, ps) -> session.executeAsync(boundStatement)); + fail("Missing CassandraConnectionFailureException"); + } catch (CassandraConnectionFailureException e) { + assertThat(e).hasMessageContaining("No node was available"); + } + + ListenableFuture> future = template.execute( + session -> AsyncResult.forExecutionException(new NoNodeAvailableException()), + (session, ps) -> session.executeAsync(boundStatement)); + + try { + future.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class) + .hasMessageContaining("No node was available"); + } + } + + @Test // DATACASS-292 + void executePreparedStatementCreatorShouldTranslateStatementCallbackExceptions() throws Exception { + + ListenableFuture future = template.execute(session -> new AsyncResult<>(preparedStatement), (session, ps) -> { + throw new NoNodeAvailableException(); + }); + + try { + future.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class) + .hasMessageContaining("No node was available"); + } + } + + @Test // DATACASS-292 + void queryPreparedStatementCreatorShouldReturnResult() { + + when(preparedStatement.bind()).thenReturn(boundStatement); + when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + + ListenableFuture> future = template.query(session -> new AsyncResult<>(preparedStatement), + (AsyncResultSetExtractor>) rs -> new AsyncResult<>(rs.currentPage())); + + assertThat(getUninterruptibly(future)).contains(row); + verify(preparedStatement).bind(); + } + + @Test // DATACASS-292 + void queryPreparedStatementCreatorAndBinderShouldReturnResult() { + + when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + + ListenableFuture future = template + .query(session -> new AsyncResult(preparedStatement), ps -> { + ps.bind("a", "b"); + return boundStatement; + }, (AsyncResultSetExtractor) AsyncResult::new); + + assertThat(getUninterruptibly(future).currentPage()).contains(row); + verify(preparedStatement).bind("a", "b"); + } + + @Test // DATACASS-292 + void queryPreparedStatementCreatorAndBinderShouldTranslatePrepareStatementExceptions() throws Exception { + + ListenableFuture future = template + .query(session -> AsyncResult.forExecutionException(new NoNodeAvailableException()), ps -> { + ps.bind("a", "b"); + return boundStatement; + }, (AsyncResultSetExtractor) AsyncResult::new); + + try { + future.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class); + } + } + + @Test // DATACASS-292 + void queryPreparedStatementCreatorAndBinderShouldTranslateBindExceptions() throws Exception { + + ListenableFuture future = template.query(session -> new AsyncResult<>(preparedStatement), ps -> { + throw new NoNodeAvailableException(); + }, (AsyncResultSetExtractor) AsyncResult::new); + + try { + future.get(); + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class); + } + } + + @Test // DATACASS-292 + void queryPreparedStatementCreatorAndBinderShouldTranslateExecutionExceptions() throws Exception { + + TestResultSetFuture resultSetFuture = TestResultSetFuture.failed(new NoNodeAvailableException()); + + when(session.executeAsync(boundStatement)).thenReturn(resultSetFuture); + + ListenableFuture future = template.query(session -> new AsyncResult<>(preparedStatement), ps -> { + ps.bind("a", "b"); + return boundStatement; + }, (AsyncResultSetExtractor) AsyncResult::new); + + try { + future.get(); + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class); + } + } + + @Test // DATACASS-292 + void queryPreparedStatementCreatorAndBinderAndMapperShouldReturnResult() { + + when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + + ListenableFuture> future = template.query(session -> new AsyncResult<>(preparedStatement), ps -> { + ps.bind("a", "b"); + return boundStatement; + }, (row, rowNum) -> row); + + assertThat(getUninterruptibly(future)).hasSize(1).contains(row); + verify(preparedStatement).bind("a", "b"); + } + + @Test // DATACASS-292 + void queryForObjectPreparedStatementShouldBeEmpty() throws Exception { + + when(session.prepareAsync(any(SimpleStatement.class))) + .thenReturn(new TestPreparedStatementFuture(preparedStatement)); + when(preparedStatement.bind("Walter")).thenReturn(boundStatement); + when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.emptyList()); + + ListenableFuture future = template.queryForObject("SELECT * FROM user WHERE username = ?", + (row, rowNum) -> "OK", "Walter"); + + try { + future.get(); + + fail("Missing IncorrectResultSizeDataAccessException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(EmptyResultDataAccessException.class) + .hasMessageContaining("expected 1, actual 0"); + } + } + + @Test // DATACASS-292 + void queryForObjectPreparedStatementShouldReturnRecord() { + + when(session.prepareAsync(any(SimpleStatement.class))) + .thenReturn(new TestPreparedStatementFuture(preparedStatement)); + when(preparedStatement.bind("Walter")).thenReturn(boundStatement); + when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + + ListenableFuture future = template.queryForObject("SELECT * FROM user WHERE username = ?", + (row, rowNum) -> "OK", "Walter"); + assertThat(getUninterruptibly(future)).isEqualTo("OK"); + } + + @Test // DATACASS-292 + void queryForObjectPreparedStatementShouldFailReturningManyRecords() throws Exception { + + when(session.prepareAsync(any(SimpleStatement.class))) + .thenReturn(new TestPreparedStatementFuture(preparedStatement)); + when(preparedStatement.bind("Walter")).thenReturn(boundStatement); + when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Arrays.asList(row, row)); + + ListenableFuture future = template.queryForObject("SELECT * FROM user WHERE username = ?", + (row, rowNum) -> "OK", "Walter"); + try { + future.get(); + + fail("Missing IncorrectResultSizeDataAccessException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(IncorrectResultSizeDataAccessException.class) + .hasMessageContaining("expected 1, actual 2"); + } + } + + @Test // DATACASS-292 + void queryForObjectPreparedStatementWithTypeShouldReturnRecord() { + + when(session.prepareAsync(any(SimpleStatement.class))) + .thenReturn(new TestPreparedStatementFuture(preparedStatement)); + when(preparedStatement.bind("Walter")).thenReturn(boundStatement); + when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + when(row.getColumnDefinitions()).thenReturn(columnDefinitions); + when(columnDefinitions.size()).thenReturn(1); + when(row.getString(0)).thenReturn("OK"); + + Future future = template.queryForObject("SELECT * FROM user WHERE username = ?", String.class, "Walter"); + + assertThat(getUninterruptibly(future)).isEqualTo("OK"); + } + + @Test // DATACASS-292 + void queryForListPreparedStatementWithTypeShouldReturnRecord() { + + when(session.prepareAsync(any(SimpleStatement.class))) + .thenReturn(new TestPreparedStatementFuture(preparedStatement)); + when(preparedStatement.bind("Walter")).thenReturn(boundStatement); + when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.currentPage()).thenReturn(Arrays.asList(row, row)); + when(row.getColumnDefinitions()).thenReturn(columnDefinitions); + when(columnDefinitions.size()).thenReturn(1); + when(row.getString(0)).thenReturn("OK", "NOT OK"); + + ListenableFuture> future = template.queryForList("SELECT * FROM user WHERE username = ?", String.class, + "Walter"); + + assertThat(getUninterruptibly(future)).contains("OK", "NOT OK"); + } + + @Test // DATACASS-292 + void updatePreparedStatementShouldReturnApplied() { + + when(session.prepareAsync(any(SimpleStatement.class))) + .thenReturn(new TestPreparedStatementFuture(preparedStatement)); + when(preparedStatement.bind("Walter")).thenReturn(boundStatement); + when(session.executeAsync(boundStatement)).thenReturn(new TestResultSetFuture(resultSet)); + when(resultSet.wasApplied()).thenReturn(true); + + ListenableFuture future = template.execute("UPDATE user SET username = ?", "Walter"); + + assertThat(getUninterruptibly(future)).isTrue(); + } + + private void doTestStrings(Consumer cqlTemplateConsumer) { + doTestStrings(null, null, null, cqlTemplateConsumer); + } + + private void doTestStrings(@Nullable Integer fetchSize, @Nullable ConsistencyLevel consistencyLevel, + @Nullable CqlIdentifier keyspace, Consumer cqlTemplateConsumer) { + + String[] results = { "Walter", "Hank", "Jesse" }; + + when(this.session.executeAsync((Statement) any())).thenReturn(new TestResultSetFuture(resultSet)); + when(this.resultSet.currentPage()).thenReturn(Arrays.asList(row, row, row)); + when(this.row.getString(0)).thenReturn(results[0], results[1], results[2]); + when(this.session.prepareAsync(anyString())).thenReturn(new TestPreparedStatementFuture(this.preparedStatement)); + when(this.session.prepareAsync(any(SimpleStatement.class))) + .thenReturn(new TestPreparedStatementFuture(this.preparedStatement)); + + AsyncCqlTemplate template = new AsyncCqlTemplate(); + template.setSession(this.session); + + if (fetchSize != null) { + template.setFetchSize(fetchSize); + } + + if (consistencyLevel != null) { + template.setConsistencyLevel(consistencyLevel); + } + + if (keyspace != null) { + template.setKeyspace(keyspace); + } + + cqlTemplateConsumer.accept(template); + + ArgumentCaptor statementArgumentCaptor = ArgumentCaptor.forClass(Statement.class); + verify(this.session).executeAsync(statementArgumentCaptor.capture()); + + Statement statement = statementArgumentCaptor.getValue(); + + if (fetchSize != null) { + assertThat(statement.getPageSize()).isEqualTo(fetchSize.intValue()); + } + + if (consistencyLevel != null) { + assertThat(statement.getConsistencyLevel()).isEqualTo(consistencyLevel); + } + } + + private static T getUninterruptibly(Future future) { + + try { + return future.get(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static class TestResultSetFuture extends CompletableFuture { + + private TestResultSetFuture() {} + + private TestResultSetFuture(AsyncResultSet result) { + complete(result); + } + + /** + * Create a completed future that reports a failure given {@link Throwable}. + * + * @param throwable must not be {@literal null}. + * @return the completed/failed {@link TestResultSetFuture}. + */ + private static TestResultSetFuture failed(Throwable throwable) { + + TestResultSetFuture future = new TestResultSetFuture(); + future.completeExceptionally(throwable); + return future; + } + } + + private static class TestPreparedStatementFuture extends CompletableFuture { + + public TestPreparedStatementFuture() {} + + private TestPreparedStatementFuture(PreparedStatement ps) { + complete(ps); + } + + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/legacy/AsyncResultStreamUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/legacy/AsyncResultStreamUnitTests.java new file mode 100644 index 000000000..3d79060ff --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/cql/legacy/AsyncResultStreamUnitTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2019-2022 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 + * + * https://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.cql.legacy; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.concurrent.ListenableFuture; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.Row; + +/** + * Unit tests for {@link AsyncResultStream}. + * + * @author Mark Paluch + */ +class AsyncResultStreamUnitTests { + + private AsyncResultSet first = mock(AsyncResultSet.class); + private AsyncResultSet last = mock(AsyncResultSet.class); + private Row row1 = mock(Row.class); + private Row row2 = mock(Row.class); + + @Test // DATACASS-656 + void shouldIterateFirstPage() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + List rows = new ArrayList<>(); + + AsyncResultStream.from(first).forEach(rows::add); + + assertThat(rows).containsOnly(row1); + } + + @Test // DATACASS-656 + void shouldIterateMappedFirstPage() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + List rows = new ArrayList<>(); + + AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum).forEach(rows::add); + + assertThat(rows).containsOnly("row-1"); + } + + @Test // DATACASS-656 + void shouldIterateMappedPages() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + when(last.currentPage()).thenReturn(Collections.singletonList(row2)); + when(first.fetchNextPage()).thenReturn(CompletableFuture.completedFuture(last)); + when(first.hasMorePages()).thenReturn(true); + + List rows = new ArrayList<>(); + + AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum).forEach(rows::add); + + assertThat(rows).containsOnly("row-1", "row-2"); + } + + @Test // DATACASS-656 + void shouldPropagateExceptionOnIterate() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally(new RuntimeException("boo")); + when(first.fetchNextPage()).thenReturn(failed); + when(first.hasMorePages()).thenReturn(true); + + List rows = new ArrayList<>(); + + ListenableFuture completion = AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum) + .forEach(rows::add); + + assertThatThrownBy(completion::get).hasRootCauseInstanceOf(RuntimeException.class); + } + + @Test // DATACASS-656 + void shouldCollectFirstPage() throws ExecutionException, InterruptedException { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + ListenableFuture> collect = AsyncResultStream.from(first).collect(Collectors.toList()); + + assertThat(collect.get()).containsOnly(row1); + } + + @Test // DATACASS-656 + void shouldCollectMappedPages() throws ExecutionException, InterruptedException { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + when(last.currentPage()).thenReturn(Collections.singletonList(row2)); + when(first.fetchNextPage()).thenReturn(CompletableFuture.completedFuture(last)); + when(first.hasMorePages()).thenReturn(true); + + ListenableFuture> rows = AsyncResultStream.from(first).map((row, rowNum) -> "row-" + rowNum) + .collect(Collectors.toList()); + + assertThat(rows.get()).containsOnly("row-1", "row-2"); + } + + @Test // DATACASS-656 + void shouldPropagateExceptionOnCollect() { + + when(first.currentPage()).thenReturn(Collections.singletonList(row1)); + + CompletableFuture failed = new CompletableFuture<>(); + failed.completeExceptionally(new RuntimeException("boo")); + when(first.fetchNextPage()).thenReturn(failed); + when(first.hasMorePages()).thenReturn(true); + + ListenableFuture> collect = AsyncResultStream.from(first).collect(Collectors.toList()); + + assertThatThrownBy(collect::get).hasRootCauseInstanceOf(RuntimeException.class); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/legacy/AsyncCassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/legacy/AsyncCassandraTemplateIntegrationTests.java new file mode 100644 index 000000000..fdafeddb4 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/legacy/AsyncCassandraTemplateIntegrationTests.java @@ -0,0 +1,380 @@ +/* + * Copyright 2022 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 + * + * https://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.legacy; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.cassandra.core.query.Criteria.*; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.core.DeleteOptions; +import org.springframework.data.cassandra.core.InsertOptions; +import org.springframework.data.cassandra.core.UpdateOptions; +import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.cql.legacy.AsyncCqlTemplate; +import org.springframework.data.cassandra.core.query.CassandraPageRequest; +import org.springframework.data.cassandra.core.query.Columns; +import org.springframework.data.cassandra.core.query.Query; +import org.springframework.data.cassandra.core.query.Update; +import org.springframework.data.cassandra.domain.User; +import org.springframework.data.cassandra.domain.UserToken; +import org.springframework.data.cassandra.repository.support.SchemaTestUtils; +import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTests; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.util.concurrent.ListenableFuture; + +import com.datastax.oss.driver.api.core.uuid.Uuids; + +/** + * Integration tests for {@link AsyncCassandraTemplate}. + * + * @author Mark Paluch + */ +class AsyncCassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingIntegrationTests { + + private AsyncCassandraTemplate template; + + @BeforeEach + void setUp() { + + MappingCassandraConverter converter = new MappingCassandraConverter(); + CassandraTemplate cassandraTemplate = new CassandraTemplate(session, converter); + template = new AsyncCassandraTemplate(new AsyncCqlTemplate(session), converter); + prepareTemplate(template); + + SchemaTestUtils.potentiallyCreateTableFor(User.class, cassandraTemplate); + SchemaTestUtils.potentiallyCreateTableFor(UserToken.class, cassandraTemplate); + SchemaTestUtils.truncate(User.class, cassandraTemplate); + SchemaTestUtils.truncate(UserToken.class, cassandraTemplate); + } + + /** + * Post-process the {@link AsyncCassandraTemplate} before running the tests. + * + * @param template + */ + void prepareTemplate(AsyncCassandraTemplate template) { + template.setUsePreparedStatements(false); + } + + @Test // DATACASS-343 + void shouldSelectByQueryWithSorting() { + + UserToken token1 = new UserToken(); + token1.setUserId(Uuids.endOf(System.currentTimeMillis())); + token1.setToken(Uuids.startOf(System.currentTimeMillis())); + token1.setUserComment("foo"); + + UserToken token2 = new UserToken(); + token2.setUserId(token1.getUserId()); + token2.setToken(Uuids.endOf(System.currentTimeMillis() + 100)); + token2.setUserComment("bar"); + + getUninterruptibly(template.insert(token1)); + getUninterruptibly(template.insert(token2)); + + Query query = Query.query(where("userId").is(token1.getUserId())).sort(Sort.by("token")); + + assertThat(getUninterruptibly(template.select(query, UserToken.class))).containsSequence(token1, token2); + } + + @Test // DATACASS-343 + void shouldSelectOneByQuery() { + + UserToken token1 = new UserToken(); + token1.setUserId(Uuids.endOf(System.currentTimeMillis())); + token1.setToken(Uuids.startOf(System.currentTimeMillis())); + token1.setUserComment("foo"); + + getUninterruptibly(template.insert(token1)); + + Query query = Query.query(where("userId").is(token1.getUserId())); + + assertThat(getUninterruptibly(template.selectOne(query, UserToken.class))).isEqualTo(token1); + } + + @Test // DATACASS-292 + void insertShouldInsertEntity() { + + User user = new User("heisenberg", "Walter", "White"); + + assertThat(getUser(user.getId())).isNull(); + + ListenableFuture insert = template.insert(user); + + assertThat(getUninterruptibly(insert)).isEqualTo(user); + assertThat(getUser(user.getId())).isEqualTo(user); + } + + @Test // DATACASS-250 + void insertShouldCreateEntityWithLwt() { + + InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + ListenableFuture> inserted = template.insert(user, + lwtOptions); + + assertThat(getUninterruptibly(inserted).wasApplied()).isTrue(); + } + + @Test // DATACASS-250 + void insertShouldNotUpdateEntityWithLwt() { + + InsertOptions lwtOptions = InsertOptions.builder().withIfNotExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + getUninterruptibly(template.insert(user, lwtOptions)); + + user.setFirstname("Walter Hartwell"); + + ListenableFuture> lwt = template.insert(user, + lwtOptions); + + assertThat(getUninterruptibly(lwt).wasApplied()).isFalse(); + assertThat(getUser(user.getId()).getFirstname()).isEqualTo("Walter"); + } + + @Test // DATACASS-292 + void shouldInsertAndCountEntities() { + + User user = new User("heisenberg", "Walter", "White"); + + User result = getUninterruptibly(template.insert(user)); + + ListenableFuture count = template.count(User.class); + assertThat(result).isSameAs(user); + assertThat(getUninterruptibly(count)).isEqualTo(1L); + } + + @Test // DATACASS-512 + void shouldInsertEntityAndCountByQuery() { + + User user = new User("heisenberg", "Walter", "White"); + + getUninterruptibly(template.insert(user)); + + assertThat(getUninterruptibly(template.count(Query.query(where("id").is("heisenberg")), User.class))).isOne(); + assertThat(getUninterruptibly(template.count(Query.query(where("id").is("foo")), User.class))).isZero(); + } + + @Test // DATACASS-512 + void shouldInsertEntityAndExistsByQuery() { + + User user = new User("heisenberg", "Walter", "White"); + + getUninterruptibly(template.insert(user)); + + assertThat(getUninterruptibly(template.exists(Query.query(where("id").is("heisenberg")), User.class))).isTrue(); + assertThat(getUninterruptibly(template.exists(Query.query(where("id").is("foo")), User.class))).isFalse(); + } + + @Test // DATACASS-292 + void updateShouldUpdateEntity() { + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + user.setFirstname("Walter Hartwell"); + + User updated = getUninterruptibly(template.update(user)); + + assertThat(updated).isNotNull(); + assertThat(getUser(user.getId())).isEqualTo(user); + } + + @Test // DATACASS-292 + void updateShouldNotCreateEntityWithLwt() { + + UpdateOptions lwtOptions = UpdateOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + + ListenableFuture> lwt = template.update(user, + lwtOptions); + + assertThat(getUninterruptibly(lwt).wasApplied()).isFalse(); + assertThat(getUser(user.getId())).isNull(); + } + + @Test // DATACASS-292 + void updateShouldUpdateEntityWithLwt() throws InterruptedException { + + UpdateOptions lwtOptions = UpdateOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + user.setFirstname("Walter Hartwell"); + + ListenableFuture> updated = template.update(user, + lwtOptions); + + assertThat(getUninterruptibly(updated).wasApplied()).isTrue(); + assertThat(getUninterruptibly(updated).getEntity()).isSameAs(user); + } + + @Test // DATACASS-343 + void updateShouldUpdateEntityByQuery() { + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + Query query = Query.query(where("id").is("heisenberg")); + boolean result = getUninterruptibly( + template.update(query, Update.empty().set("firstname", "Walter Hartwell"), User.class)); + assertThat(result).isTrue(); + + assertThat(getUser(user.getId()).getFirstname()).isEqualTo("Walter Hartwell"); + } + + @Test // DATACASS-343 + void deleteByQueryShouldRemoveEntity() { + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + Query query = Query.query(where("id").is("heisenberg")); + assertThat(getUninterruptibly(template.delete(query, User.class))).isTrue(); + + assertThat(getUser(user.getId())).isNull(); + } + + @Test // DATACASS-343 + void deleteColumnsByQueryShouldRemoveColumn() { + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + Query query = Query.query(where("id").is("heisenberg")).columns(Columns.from("lastname")); + + assertThat(getUninterruptibly(template.delete(query, User.class))).isTrue(); + + User loaded = getUser(user.getId()); + assertThat(loaded.getFirstname()).isEqualTo("Walter"); + assertThat(loaded.getLastname()).isNull(); + } + + @Test // DATACASS-292 + void deleteShouldRemoveEntity() { + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + User deleted = getUninterruptibly(template.delete(user)); + + assertThat(deleted).isNotNull(); + assertThat(getUser(user.getId())).isNull(); + } + + @Test // DATACASS-292 + void deleteByIdShouldRemoveEntity() { + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + Boolean deleted = getUninterruptibly(template.deleteById(user.getId(), User.class)); + assertThat(deleted).isTrue(); + + assertThat(getUser(user.getId())).isNull(); + } + + @Test // DATACASS-606 + void deleteShouldRemoveEntityWithLwt() { + + DeleteOptions lwtOptions = DeleteOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + assertThat(getUninterruptibly(template.delete(user, lwtOptions)).wasApplied()).isTrue(); + } + + @Test // DATACASS-606 + void deleteByQueryShouldRemoveEntityWithLwt() { + + DeleteOptions lwtOptions = DeleteOptions.builder().withIfExists().build(); + + User user = new User("heisenberg", "Walter", "White"); + getUninterruptibly(template.insert(user)); + + Query query = Query.query(where("id").is("heisenberg")).queryOptions(lwtOptions); + assertThat(getUninterruptibly(template.delete(query, User.class))).isTrue(); + assertThat(getUninterruptibly(template.delete(query, User.class))).isFalse(); + } + + @Test // DATACASS-56 + void shouldPageRequests() { + + Set expectedIds = new LinkedHashSet<>(); + + for (int count = 0; count < 100; count++) { + User user = new User("heisenberg" + count, "Walter", "White"); + expectedIds.add(user.getId()); + getUninterruptibly(template.insert(user)); + } + + Set ids = new HashSet<>(); + + Query query = Query.empty(); + + Slice slice = getUninterruptibly( + template.slice(query.pageRequest(CassandraPageRequest.first(10)), User.class)); + + int iterations = 0; + + do { + + iterations++; + + assertThat(slice).hasSize(10); + + slice.stream().map(User::getId).forEach(ids::add); + + if (slice.hasNext()) { + slice = getUninterruptibly(template.slice(query.pageRequest(slice.nextPageable()), User.class)); + } else { + break; + } + } while (!slice.getContent().isEmpty()); + + assertThat(ids).containsAll(expectedIds); + assertThat(iterations).isEqualTo(10); + } + + private User getUser(String id) { + return getUninterruptibly(template.selectOneById(id, User.class)); + } + + private static T getUninterruptibly(Future future) { + + try { + return future.get(); + } catch (Exception cause) { + throw new IllegalStateException(cause); + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/legacy/AsyncCassandraTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/legacy/AsyncCassandraTemplateUnitTests.java new file mode 100644 index 000000000..29471ee89 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/legacy/AsyncCassandraTemplateUnitTests.java @@ -0,0 +1,605 @@ +/* + * Copyright 2022 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 + * + * https://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.legacy; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.cassandra.core.query.Criteria.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import org.springframework.data.cassandra.CassandraConnectionFailureException; +import org.springframework.data.cassandra.core.DeleteOptions; +import org.springframework.data.cassandra.core.UpdateOptions; +import org.springframework.data.cassandra.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.cassandra.core.mapping.event.BeforeSaveCallback; +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.domain.User; +import org.springframework.data.cassandra.domain.VersionedUser; +import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.util.concurrent.ListenableFuture; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.NoNodeAvailableException; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; + +/** + * Unit tests for {@link AsyncCassandraTemplate}. + * + * @author Mark Paluch + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class AsyncCassandraTemplateUnitTests { + + @Mock CqlSession session; + CodecRegistry codecRegistry = new DefaultCodecRegistry("foo"); + @Mock DriverContext driverContext; + @Mock AsyncResultSet resultSet; + @Mock Row row; + @Mock ColumnDefinition columnDefinition; + @Mock ColumnDefinitions columnDefinitions; + + @Captor ArgumentCaptor statementCaptor; + + private AsyncCassandraTemplate template; + + private Object beforeSave; + + private Object beforeConvert; + + @BeforeEach + void setUp() { + + when(driverContext.getCodecRegistry()).thenReturn(codecRegistry); + when(session.getContext()).thenReturn(driverContext); + + when(session.executeAsync(any(Statement.class))).thenReturn(new TestResultSetFuture(resultSet)); + when(row.getColumnDefinitions()).thenReturn(columnDefinitions); + + EntityCallbacks callbacks = EntityCallbacks.create(); + callbacks.addEntityCallback((BeforeSaveCallback) (entity, tableName, statement) -> { + + assertThat(tableName).isNotNull(); + assertThat(statement).isNotNull(); + beforeSave = entity; + return entity; + }); + + callbacks.addEntityCallback((BeforeConvertCallback) (entity, tableName) -> { + + assertThat(tableName).isNotNull(); + beforeConvert = entity; + return entity; + }); + + template = new AsyncCassandraTemplate(session); + template.setUsePreparedStatements(false); + template.setEntityCallbacks(callbacks); + } + + @Test // gh-1133 + void shouldConfigureConverterFromSession() { + assertThat(template.getConverter().getCodecRegistry()).isEqualTo(session.getContext().getCodecRegistry()); + assertThat(template.getConverter()).extracting("userTypeResolver").isNotNull(); + } + + @Test // DATACASS-292 + void selectUsingCqlShouldReturnMappedResults() { + + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + when(columnDefinitions.contains(any(CqlIdentifier.class))).thenReturn(true); + + when(columnDefinitions.get(anyInt())).thenReturn(columnDefinition); + when(columnDefinitions.firstIndexOf("id")).thenReturn(0); + when(columnDefinitions.firstIndexOf("firstname")).thenReturn(1); + when(columnDefinitions.firstIndexOf("lastname")).thenReturn(2); + + when(columnDefinition.getType()).thenReturn(DataTypes.TEXT); + + when(row.getObject(0)).thenReturn("myid"); + when(row.getObject(1)).thenReturn("Walter"); + when(row.getObject(2)).thenReturn("White"); + + ListenableFuture> list = template.select("SELECT * FROM users", User.class); + + assertThat(getUninterruptibly(list)).hasSize(1).contains(new User("myid", "Walter", "White")); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("SELECT * FROM users"); + } + + @Test // DATACASS-292 + void selectUsingCqlShouldInvokeCallbackWithMappedResults() { + + when(resultSet.currentPage()).thenReturn(Collections.singletonList(row)); + when(columnDefinitions.contains(any(CqlIdentifier.class))).thenReturn(true); + when(columnDefinitions.get(anyInt())).thenReturn(columnDefinition); + when(columnDefinitions.firstIndexOf("id")).thenReturn(0); + when(columnDefinitions.firstIndexOf("firstname")).thenReturn(1); + when(columnDefinitions.firstIndexOf("lastname")).thenReturn(2); + + when(columnDefinition.getType()).thenReturn(DataTypes.TEXT); + + when(row.getObject(0)).thenReturn("myid"); + when(row.getObject(1)).thenReturn("Walter"); + when(row.getObject(2)).thenReturn("White"); + + List list = new ArrayList<>(); + + ListenableFuture result = template.select("SELECT * FROM users", list::add, User.class); + + assertThat(getUninterruptibly(result)).isNull(); + assertThat(list).hasSize(1).contains(new User("myid", "Walter", "White")); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("SELECT * FROM users"); + } + + @Test // DATACASS-292 + void selectShouldTranslateException() throws Exception { + + when(resultSet.currentPage()).thenThrow(new NoNodeAvailableException()); + + ListenableFuture> list = template.select("SELECT * FROM users", User.class); + + try { + list.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class) + .hasRootCauseInstanceOf(NoNodeAvailableException.class); + } + } + + @Test // DATACASS-292 + void selectOneShouldReturnMappedResults() { + + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + when(columnDefinitions.contains(any(CqlIdentifier.class))).thenReturn(true); + + when(columnDefinitions.get(anyInt())).thenReturn(columnDefinition); + when(columnDefinitions.firstIndexOf("id")).thenReturn(0); + when(columnDefinitions.firstIndexOf("firstname")).thenReturn(1); + when(columnDefinitions.firstIndexOf("lastname")).thenReturn(2); + + when(columnDefinition.getType()).thenReturn(DataTypes.TEXT); + + when(row.getObject(0)).thenReturn("myid"); + when(row.getObject(1)).thenReturn("Walter"); + when(row.getObject(2)).thenReturn("White"); + + ListenableFuture future = template.selectOne("SELECT * FROM users WHERE id='myid'", User.class); + + assertThat(getUninterruptibly(future)).isEqualTo(new User("myid", "Walter", "White")); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("SELECT * FROM users WHERE id='myid'"); + } + + @Test // DATACASS-292 + void selectOneByIdShouldReturnMappedResults() { + + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + when(columnDefinitions.contains(any(CqlIdentifier.class))).thenReturn(true); + when(columnDefinitions.get(anyInt())).thenReturn(columnDefinition); + when(columnDefinitions.firstIndexOf("id")).thenReturn(0); + when(columnDefinitions.firstIndexOf("firstname")).thenReturn(1); + when(columnDefinitions.firstIndexOf("lastname")).thenReturn(2); + + when(columnDefinition.getType()).thenReturn(DataTypes.ASCII); + + when(row.getObject(0)).thenReturn("myid"); + when(row.getObject(1)).thenReturn("Walter"); + when(row.getObject(2)).thenReturn("White"); + + ListenableFuture future = template.selectOneById("myid", User.class); + + assertThat(getUninterruptibly(future)).isEqualTo(new User("myid", "Walter", "White")); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("SELECT * FROM users WHERE id='myid' LIMIT 1"); + } + + @Test // DATACASS-696 + void selectOneShouldNull() { + + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + + ListenableFuture future = template.selectOne("SELECT id FROM users WHERE id='myid'", String.class); + + assertThat(getUninterruptibly(future)).isNull(); + } + + @Test // DATACASS-292 + void existsShouldReturnExistingElement() { + + when(resultSet.one()).thenReturn(row); + + ListenableFuture future = template.exists("myid", User.class); + + assertThat(getUninterruptibly(future)).isTrue(); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("SELECT * FROM users WHERE id='myid' LIMIT 1"); + } + + @Test // DATACASS-292 + void existsShouldReturnNonExistingElement() { + + ListenableFuture future = template.exists("myid", User.class); + + assertThat(getUninterruptibly(future)).isFalse(); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("SELECT * FROM users WHERE id='myid' LIMIT 1"); + } + + @Test // DATACASS-512 + void existsByQueryShouldReturnExistingElement() { + + when(resultSet.one()).thenReturn(row); + + ListenableFuture future = template.exists(Query.empty(), User.class); + + assertThat(getUninterruptibly(future)).isTrue(); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("SELECT * FROM users LIMIT 1"); + } + + @Test // DATACASS-292 + void countShouldExecuteCountQueryElement() { + + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + when(row.getLong(0)).thenReturn(42L); + when(columnDefinitions.size()).thenReturn(1); + + ListenableFuture future = template.count(User.class); + + assertThat(getUninterruptibly(future)).isEqualTo(42L); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("SELECT count(1) FROM users"); + } + + @Test // DATACASS-292 + void countByQueryShouldExecuteCountQueryElement() { + + when(resultSet.currentPage()).thenReturn(Collections.singleton(row)); + when(row.getLong(0)).thenReturn(42L); + when(columnDefinitions.size()).thenReturn(1); + + ListenableFuture future = template.count(Query.empty(), User.class); + + assertThat(getUninterruptibly(future)).isEqualTo(42L); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("SELECT count(1) FROM users"); + } + + @Test // DATACASS-292, DATACASS-618 + void insertShouldInsertEntity() { + + when(resultSet.wasApplied()).thenReturn(true); + + User user = new User("heisenberg", "Walter", "White"); + + ListenableFuture future = template.insert(user); + + assertThat(getUninterruptibly(future)).isEqualTo(user); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())) + .isEqualTo("INSERT INTO users (firstname,id,lastname) VALUES ('Walter','heisenberg','White')"); + assertThat(beforeConvert).isSameAs(user); + assertThat(beforeSave).isSameAs(user); + } + + @Test // DATACASS-618 + void insertShouldInsertVersionedEntity() { + + when(resultSet.wasApplied()).thenReturn(true); + + VersionedUser user = new VersionedUser("heisenberg", "Walter", "White"); + + ListenableFuture future = template.insert(user); + + assertThat(getUninterruptibly(future)).isEqualTo(user); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo( + "INSERT INTO vusers (firstname,id,lastname,version) VALUES ('Walter','heisenberg','White',0) IF NOT EXISTS"); + assertThat(beforeConvert).isSameAs(user); + assertThat(beforeSave).isSameAs(user); + } + + @Test // DATACASS-292 + void insertShouldTranslateException() throws Exception { + + reset(session); + when(session.executeAsync(any(Statement.class))) + .thenReturn(TestResultSetFuture.failed(new NoNodeAvailableException())); + + ListenableFuture future = template.insert(new User("heisenberg", "Walter", "White")); + + try { + future.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class) + .hasRootCauseInstanceOf(NoNodeAvailableException.class); + } + } + + @Test // DATACASS-292, DATACASS-618 + void updateShouldUpdateEntity() { + + when(resultSet.wasApplied()).thenReturn(true); + + User user = new User("heisenberg", "Walter", "White"); + + ListenableFuture future = template.update(user); + + assertThat(getUninterruptibly(future)).isEqualTo(user); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())) + .isEqualTo("UPDATE users SET firstname='Walter', lastname='White' WHERE id='heisenberg'"); + assertThat(beforeConvert).isSameAs(user); + assertThat(beforeSave).isSameAs(user); + } + + @Test // DATACASS-618 + void updateShouldUpdateVersionedEntity() { + + when(resultSet.wasApplied()).thenReturn(true); + + VersionedUser user = new VersionedUser("heisenberg", "Walter", "White"); + user.setVersion(0L); + + ListenableFuture future = template.update(user); + + assertThat(getUninterruptibly(future)).isEqualTo(user); + verify(session).executeAsync(statementCaptor.capture()); + SimpleStatement value = statementCaptor.getValue(); + assertThat(render(value)).isEqualTo( + "UPDATE vusers SET firstname='Walter', lastname='White', version=1 WHERE id='heisenberg' IF version=0"); + assertThat(beforeConvert).isSameAs(user); + assertThat(beforeSave).isSameAs(user); + } + + @Test // DATACASS-575 + void updateShouldUpdateEntityWithOptions() { + + UpdateOptions updateOptions = UpdateOptions.builder().withIfExists().build(); + User user = new User("heisenberg", "Walter", "White"); + + template.update(user, updateOptions); + + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())) + .isEqualTo("UPDATE users SET firstname='Walter', lastname='White' WHERE id='heisenberg' IF EXISTS"); + } + + @Test // DATACASS-575 + void updateShouldUpdateEntityWithLwt() { + + UpdateOptions options = UpdateOptions.builder().ifCondition(where("firstname").is("Walter")).build(); + User user = new User("heisenberg", "Walter", "White"); + + template.update(user, options); + + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())) + .isEqualTo("UPDATE users SET firstname='Walter', lastname='White' WHERE id='heisenberg' IF firstname='Walter'"); + } + + @Test // DATACASS-575 + void updateShouldApplyUpdateQuery() { + + Query query = Query.query(where("id").is("heisenberg")); + Update update = Update.update("firstname", "Walter"); + + template.update(query, update, User.class); + + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())) + .isEqualTo("UPDATE users SET firstname='Walter' WHERE id='heisenberg'"); + } + + @Test // DATACASS-575 + void updateShouldApplyUpdateQueryWitLwt() { + + Filter ifCondition = Filter.from(where("firstname").is("Walter"), where("lastname").is("White")); + + Query query = Query.query(where("id").is("heisenberg")) + .queryOptions(UpdateOptions.builder().ifCondition(ifCondition).build()); + + Update update = Update.update("firstname", "Walter"); + + template.update(query, update, User.class); + + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo( + "UPDATE users SET firstname='Walter' WHERE id='heisenberg' IF firstname='Walter' AND lastname='White'"); + } + + @Test // DATACASS-292 + void updateShouldTranslateException() throws Exception { + + reset(session); + when(session.executeAsync(any(Statement.class))) + .thenReturn(TestResultSetFuture.failed(new NoNodeAvailableException())); + + ListenableFuture future = template.update(new User("heisenberg", "Walter", "White")); + + try { + future.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class) + .hasRootCauseInstanceOf(NoNodeAvailableException.class); + } + } + + @Test // DATACASS-292 + void deleteByIdShouldRemoveEntity() { + + when(resultSet.wasApplied()).thenReturn(true); + + User user = new User("heisenberg", "Walter", "White"); + + ListenableFuture future = template.deleteById(user.getId(), User.class); + + assertThat(getUninterruptibly(future)).isTrue(); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("DELETE FROM users WHERE id='heisenberg'"); + } + + @Test // DATACASS-292 + void deleteShouldRemoveEntity() { + + when(resultSet.wasApplied()).thenReturn(true); + + User user = new User("heisenberg", "Walter", "White"); + + ListenableFuture future = template.delete(user); + + assertThat(getUninterruptibly(future)).isEqualTo(user); + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("DELETE FROM users WHERE id='heisenberg'"); + } + + @Test // DATACASS-575 + void deleteShouldRemoveEntityWithLwt() { + + User user = new User("heisenberg", "Walter", "White"); + DeleteOptions options = DeleteOptions.builder().ifCondition(where("firstname").is("Walter")).build(); + + template.delete(user, options); + + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())) + .isEqualTo("DELETE FROM users WHERE id='heisenberg' IF firstname='Walter'"); + } + + @Test // DATACASS-575 + void deleteShouldRemoveByQueryWithLwt() { + + DeleteOptions options = DeleteOptions.builder().ifCondition(where("firstname").is("Walter")).build(); + Query query = Query.query(where("id").is("heisenberg")).queryOptions(options); + + template.delete(query, User.class); + + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())) + .isEqualTo("DELETE FROM users WHERE id='heisenberg' IF firstname='Walter'"); + } + + @Test // DATACASS-292 + void deleteShouldTranslateException() throws Exception { + + reset(session); + when(session.executeAsync(any(Statement.class))) + .thenReturn(TestResultSetFuture.failed(new NoNodeAvailableException())); + + ListenableFuture future = template.delete(new User("heisenberg", "Walter", "White")); + + try { + future.get(); + + fail("Missing CassandraConnectionFailureException"); + } catch (ExecutionException e) { + assertThat(e).hasCauseInstanceOf(CassandraConnectionFailureException.class) + .hasRootCauseInstanceOf(NoNodeAvailableException.class); + } + } + + @Test // DATACASS-292 + void truncateShouldRemoveEntities() { + + template.truncate(User.class); + + verify(session).executeAsync(statementCaptor.capture()); + assertThat(render(statementCaptor.getValue())).isEqualTo("TRUNCATE users"); + } + + private static String render(SimpleStatement statement) { + + String query = statement.getQuery(); + List positionalValues = statement.getPositionalValues(); + for (Object positionalValue : positionalValues) { + + query = query.replaceFirst("\\?", + positionalValue != null + ? CodecRegistry.DEFAULT.codecFor((Class) positionalValue.getClass()).format(positionalValue) + : "NULL"); + } + + return query; + } + + private static T getUninterruptibly(Future future) { + + try { + return future.get(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static class TestResultSetFuture extends CompletableFuture { + + private TestResultSetFuture() {} + + private TestResultSetFuture(AsyncResultSet resultSet) { + complete(resultSet); + } + + /** + * Create a completed future that reports a failure given {@link Throwable}. + * + * @param throwable must not be {@literal null}. + * @return the completed/failed {@link TestResultSetFuture}. + */ + private static TestResultSetFuture failed(Throwable throwable) { + + TestResultSetFuture future = new TestResultSetFuture(); + future.completeExceptionally(throwable); + return future; + } + } +} diff --git a/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/core/cql/AsyncCqlOperationsExtensionsUnitTests.kt b/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/core/cql/LegacyAsyncCqlOperationsExtensionsUnitTests.kt similarity index 94% rename from spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/core/cql/AsyncCqlOperationsExtensionsUnitTests.kt rename to spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/core/cql/LegacyAsyncCqlOperationsExtensionsUnitTests.kt index f9d030093..2a9d5a4e3 100644 --- a/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/core/cql/AsyncCqlOperationsExtensionsUnitTests.kt +++ b/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/core/cql/LegacyAsyncCqlOperationsExtensionsUnitTests.kt @@ -21,14 +21,14 @@ import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.Test import org.springframework.data.cassandra.domain.Person -import org.springframework.scheduling.annotation.AsyncResult +import java.util.concurrent.CompletableFuture /** * Unit tests for [AsyncCqlOperationsExtensions]. * * @author Mark Paluch */ -class AsyncCqlOperationsExtensionsUnitTests { +class LegacyAsyncCqlOperationsExtensionsUnitTests { val operations = mockk(relaxed = true) @@ -120,7 +120,14 @@ class AsyncCqlOperationsExtensionsUnitTests { @Test // DATACASS-484 fun `query(String, ResultSetExtractor, array) extension should call its Java counterpart`() { - operations.query("", 3) { rs -> AsyncResult(Person("Walter", rs.one()!!.getString(0))) } + operations.query("", 3) { rs -> + CompletableFuture.completedFuture( + Person( + "Walter", + rs.one()!!.getString(0) + ) + ) + } verify { operations.query(eq(""), any>(), eq(3)) } } diff --git a/src/main/asciidoc/reference/migration-guide-3.0-to-4.0.adoc b/src/main/asciidoc/reference/migration-guide-3.0-to-4.0.adoc new file mode 100644 index 000000000..c5ca0e199 --- /dev/null +++ b/src/main/asciidoc/reference/migration-guide-3.0-to-4.0.adoc @@ -0,0 +1,10 @@ +[[cassandra.migration.3.x-to-4.x]] += Migration Guide from Spring Data Cassandra 3.x to 4.x + +Spring Data for Apache Cassandra 4.0 introduces a set of breaking changes when upgrading from earlier versions. + +== Asynchronous Template API + +With the deprecation of `ListenableFuture`, `AsyncCqlOperations` and `AsyncCassandraOperations` and their dependant classes were migrated to `CompletableFuture`. +If your application heavily depends on `ListenableFuture` and you cannot easily migrate to `CompletableFuture` then we suggest switching to the legacy `Async…Operations` types in the `legacy` subpackage. +That is `org.springframework.data.cassandra.core.cql.legacy` for `AsyncCqlOperations` and `org.springframework.data.cassandra.core.legacy` for `AsyncCassandraOperations`. diff --git a/src/main/asciidoc/reference/migration-guides.adoc b/src/main/asciidoc/reference/migration-guides.adoc index 72b0f9f44..c7f13c632 100644 --- a/src/main/asciidoc/reference/migration-guides.adoc +++ b/src/main/asciidoc/reference/migration-guides.adoc @@ -4,3 +4,5 @@ include::migration-guide-1.5-to-2.0.adoc[leveloffset=+1] include::migration-guide-2.2-to-3.0.adoc[leveloffset=+1] + +include::migration-guide-3.0-to-4.0.adoc[leveloffset=+1]