diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java index 296b33055..eb3dbb7ad 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java @@ -62,6 +62,14 @@ public abstract class AbstractCassandraConverter implements CassandraConverter, this.conversions = conversions; } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.convert.CassandraConverter#getCustomConversions() + */ + @Override + public CustomConversions getCustomConversions() { + return conversions; + } + /* (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverter.java index 9c73cb7c1..ec2e9a13c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverter.java @@ -46,7 +46,7 @@ public interface CassandraConverter *
  • A the composite primary key for {@link org.springframework.data.cassandra.mapping.PrimaryKey} using a * {@link org.springframework.data.cassandra.mapping.PrimaryKeyClass}
  • * - * + * * @param object must not be {@literal null}. * @param entity must not be {@literal null}. * @return @@ -55,10 +55,17 @@ public interface CassandraConverter /** * Converts and writes a {@code source} object into a {@code sink} using the given {@link CassandraPersistentEntity}. - * + * * @param source the source, may be {@literal null}. * @param sink must not be {@literal null}. * @param entity must not be {@literal null}. */ void write(Object source, Object sink, CassandraPersistentEntity entity); + + /** + * Returns the {@link CustomConversions} registered in the {@link CassandraConverter}. + * + * @return the {@link CustomConversions}. + */ + CustomConversions getCustomConversions(); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java index 9f129a84b..204ca8ba9 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java @@ -42,6 +42,10 @@ public class BasicCassandraPersistentEntityMetadataVerifier implements Cassandra @Override public void verify(CassandraPersistentEntity entity) throws MappingException { + if(entity.getType().isInterface()){ + return; + } + VerifierMappingExceptions exceptions = new VerifierMappingExceptions(entity, String.format("Mapping Exceptions from BasicCassandraPersistentEntityMetadataVerifier for %s", entity.getName())); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java index 387b4bead..274d1ab03 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentProperty.java @@ -51,6 +51,7 @@ import com.datastax.driver.core.DataType; * @author Alex Shvid * @author Matthew T. Adams * @author Antoine Toulme + * @author Mark Paluch */ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentProperty implements CassandraPersistentProperty, ApplicationContextAware { @@ -129,7 +130,7 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP List columnNames = getColumnNames(); if (columnNames.size() != 1) { - throw new IllegalStateException("property does not have a single column mapping"); + throw new IllegalStateException(String.format("Property [%s] has no single column mapping", getName())); } return columnNames.get(0); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java index 5bcb81e85..4d251d232 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; @@ -28,6 +29,7 @@ import org.slf4j.LoggerFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.CollectionExecution; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultProcessingConverter; @@ -38,6 +40,7 @@ import org.springframework.data.cassandra.repository.query.CassandraQueryExecuti import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -65,22 +68,29 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { */ public AbstractCassandraQuery(CassandraQueryMethod method, CassandraOperations operations) { - Assert.notNull(operations); - Assert.notNull(method); + Assert.notNull(method, "CassandraQueryMethod must not be null"); + Assert.notNull(operations, "CassandraOperations must not be null"); this.method = method; this.template = operations; } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() + */ @Override public CassandraQueryMethod getQueryMethod() { return method; } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) + */ @Override public Object execute(Object[] parameters) { - CassandraParameterAccessor accessor = new ConvertingParameterAccessor(template.getConverter(), new CassandraParametersParameterAccessor(method, parameters)); + CassandraParameterAccessor accessor = new ConvertingParameterAccessor(template.getConverter(), + new CassandraParametersParameterAccessor(method, parameters)); String query = createQuery(accessor); ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); @@ -88,7 +98,13 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { CassandraQueryExecution cassandraQueryExecution = getExecution(query, accessor, new ResultProcessingConverter(processor)); - return cassandraQueryExecution.execute(query, processor.getReturnedType().getReturnedType()); + CassandraReturnedType returnedType = new CassandraReturnedType(processor.getReturnedType(), template.getConverter().getCustomConversions()); + + if (returnedType.isProjecting()) { + return cassandraQueryExecution.execute(query, returnedType.getDomainType()); + } + + return cassandraQueryExecution.execute(query, returnedType.getReturnedType()); } /** @@ -167,6 +183,7 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { return object; } + @Deprecated protected void warnIfMoreResults(Iterator iterator) { if (log.isWarnEnabled() && iterator.hasNext()) { @@ -180,6 +197,7 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { } } + @Deprecated public ConversionService getConversionService() { return template.getConverter().getConversionService(); } @@ -200,4 +218,48 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { * @param accessor must not be {@literal null}. */ protected abstract String createQuery(CassandraParameterAccessor accessor); + + private class CassandraReturnedType { + + private final ReturnedType returnedType; + private final CustomConversions customConversions; + + CassandraReturnedType(ReturnedType returnedType, CustomConversions customConversions) { + this.returnedType = returnedType; + this.customConversions = customConversions; + } + + boolean isProjecting(){ + + if(!returnedType.isProjecting()){ + return false; + } + + // Spring Data Cassandra allows List and Map declarations on query methods + // so we don't want to let projection kick in + if(ClassUtils.isAssignable(Map.class, returnedType.getReturnedType())){ + return false; + } + + // Type conversion using registered conversions is handled on template level + if(customConversions.hasCustomWriteTarget(returnedType.getReturnedType())){ + return false; + } + + // Don't apply projection on Cassandra simple types + if(customConversions.isSimpleType(returnedType.getReturnedType())){ + return false; + } + + return true; + } + + Class getReturnedType() { + return returnedType.getReturnedType(); + } + + Class getDomainType() { + return returnedType.getDomainType(); + } + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java index 1575e2858..745c4106c 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java @@ -15,6 +15,7 @@ */ package org.springframework.data.cassandra.repository.query; +import org.springframework.data.cassandra.mapping.CassandraType; import org.springframework.data.repository.query.ParameterAccessor; import com.datastax.driver.core.DataType; @@ -32,7 +33,7 @@ public interface CassandraParameterAccessor extends ParameterAccessor { * Returns the Cassandra {@link DataType} for the declared parameter if the type is a * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder simple type}. Parameter types may be * specified using {@link org.springframework.data.cassandra.mapping.CassandraType}. - * + * * @param index the parameter index * @return the Cassandra {@link DataType} or {@literal null} if the parameter type cannot be determined from * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder} @@ -41,6 +42,16 @@ public interface CassandraParameterAccessor extends ParameterAccessor { */ DataType getDataType(int index); + /** + * Returns the {@link CassandraType} for the declared method parameter. + * + * @param index the parameter index + * @return the Cassandra {@link CassandraType} or {@literal null}. + * @see org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder + * @see org.springframework.data.cassandra.mapping.CassandraType + */ + CassandraType findCassandraType(int index); + /** * The actual parameter type (after unwrapping). * diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java index e08dfedb1..dc9048711 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java @@ -71,7 +71,7 @@ public class CassandraParameters extends Parameters { - private static final Logger LOG = LoggerFactory.getLogger(CassandraQueryCreator.class); - private final CassandraParameterAccessor accessor; + private static final Pattern PUNCTATION_PATTERN = Pattern.compile("\\p{Punct}"); + private static final Logger LOG = LoggerFactory.getLogger(CassandraQueryCreator.class); private final CassandraMappingContext context; + private final WhereBuilder whereBuilder = new WhereBuilder(); + private final CassandraPersistentEntity entity; /** * Creates a new {@link CassandraQueryCreator} from the given {@link PartTree}, {@link ConvertingParameterAccessor} * and {@link MappingContext}. * - * @param tree - * @param accessor - * @param context + * @param tree must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @param context must not be {@literal null}. + * @param entityMetadata must not be {@literal null}. */ - public CassandraQueryCreator(PartTree tree, CassandraParameterAccessor accessor, CassandraMappingContext context) { + public CassandraQueryCreator(PartTree tree, CassandraParameterAccessor accessor, CassandraMappingContext context, + EntityMetadata entityMetadata) { super(tree, accessor); - Assert.notNull(context); + Assert.notNull(context, "CassandraMappingContext must not be null"); + Assert.notNull(entityMetadata, "EntityInformation must not be null"); - this.accessor = accessor; this.context = context; + this.entity = context.getPersistentEntity(entityMetadata.getJavaType()); } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#create(org.springframework.data.repository.query.parser.Part, java.util.Iterator) + */ @Override protected Clause create(Part part, Iterator iterator) { PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CassandraPersistentProperty property = path.getLeafProperty(); - Clause criteria = from(part, property, - null /* TODO where(path.toDotPath(CassandraPersistentProperty.PropertyToFieldNameConverter.INSTANCE))*/, - iterator); - - return criteria; + return from(part, property, (PotentiallyConvertingIterator) iterator); } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#and(org.springframework.data.repository.query.parser.Part, java.lang.Object, java.util.Iterator) + */ @Override protected Clause and(Part part, Clause base, Iterator iterator) { if (base == null) { - return create(part, iterator); + return whereBuilder.and(create(part, iterator)); } - PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); - CassandraPersistentProperty property = path.getLeafProperty(); - - return from(part, property, - null /* TODO base.and(path.toDotPath(CassandraPersistentProperty.PropertyToFieldNameConverter.INSTANCE))*/, - iterator); + whereBuilder.and(base); + return create(part, iterator); } + /* + * Cassandra does not support OR queries. + * + * (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#or(java.lang.Object, java.lang.Object) + */ @Override protected Clause or(Clause base, Clause criteria) { throw new InvalidDataAccessApiUsageException(String.format("Cassandra does not support an OR operator!")); } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.parser.AbstractQueryCreator#complete(java.lang.Object, org.springframework.data.domain.Sort) + */ @Override protected Select complete(Clause criteria, Sort sort) { - if (criteria == null) { - return null; + if (criteria != null) { + whereBuilder.and(criteria); } - Select select = QueryBuilder.select().all().from("TODO"); - select.where(criteria); + Select select = StatementBuilder.select(entity, whereBuilder, sort); if (LOG.isDebugEnabled()) { - LOG.debug("Created query " + select.getQueryString()); + LOG.debug("Created query {}", select); } return select; } - private Clause from(Part part, CassandraPersistentProperty property, Clause criteria, Iterator parameters) { + private Clause from(Part part, CassandraPersistentProperty property, PotentiallyConvertingIterator parameters) { Type type = part.getType(); switch (type) { - // TODO - // case AFTER: - // case GREATER_THAN: - // return criteria.gt(parameters.nextConverted(property)); - // case GREATER_THAN_EQUAL: - // return criteria.gte(parameters.nextConverted(property)); - // case BEFORE: - // case LESS_THAN: - // return criteria.lt(parameters.nextConverted(property)); - // case LESS_THAN_EQUAL: - // return criteria.lte(parameters.nextConverted(property)); - // case BETWEEN: - // return criteria.gt(parameters.nextConverted(property)).lt(parameters.nextConverted(property)); - // case IS_NOT_NULL: - // return criteria.ne(null); - // case IS_NULL: - // return criteria.is(null); - // case NOT_IN: - // return criteria.nin(nextAsArray(parameters, property)); - // case IN: - // return criteria.in(nextAsArray(parameters, property)); - // case LIKE: - // case STARTING_WITH: - // case ENDING_WITH: - // case CONTAINING: - // return addAppropriateLikeRegexTo(criteria, part, parameters.next().toString()); - // case REGEX: - // return criteria.regex(parameters.next().toString()); - // case EXISTS: - // return criteria.exists((Boolean) parameters.next()); - // case TRUE: - // return criteria.is(true); - // case FALSE: - // return criteria.is(false); - // case WITHIN: - // - // Object parameter = parameters.next(); - // return criteria.within((Shape) parameter); - // case SIMPLE_PROPERTY: - // - // return isSimpleComparisionPossible(part) ? criteria.is(parameters.nextConverted(property)) - // : createLikeRegexCriteriaOrThrow(part, property, criteria, parameters, false); - // - // case NEGATING_SIMPLE_PROPERTY: - // - // return isSimpleComparisionPossible(part) ? criteria.ne(parameters.nextConverted(property)) - // : createLikeRegexCriteriaOrThrow(part, property, criteria, parameters, true); + case AFTER: + case GREATER_THAN: + return QueryBuilder.gt(columnName(property), parameters.nextConverted(property)); + case GREATER_THAN_EQUAL: + return QueryBuilder.gte(columnName(property), parameters.nextConverted(property)); + case BEFORE: + case LESS_THAN: + return QueryBuilder.lt(columnName(property), parameters.nextConverted(property)); + case LESS_THAN_EQUAL: + return QueryBuilder.lte(columnName(property), parameters.nextConverted(property)); + case IN: + return QueryBuilder.in(columnName(property), nextAsArray(property, parameters)); + case LIKE: + case STARTING_WITH: + case ENDING_WITH: + return QueryBuilder.like(columnName(property), like(type, parameters.nextConverted(property))); + case CONTAINING: + return containing(property, parameters.nextConverted(property)); + case TRUE: + return QueryBuilder.eq(columnName(property), true); + case FALSE: + return QueryBuilder.eq(columnName(property), false); + case SIMPLE_PROPERTY: + return QueryBuilder.eq(columnName(property), parameters.nextConverted(property)); default: - throw new UnsupportedCassandraQueryOperatorException(String.format("")); + throw new InvalidDataAccessApiUsageException( + String.format("Unsupported Keyword: [%s] in part [%s]", type, part)); } } - private boolean isSimpleComparisionPossible(Part part) { + private Clause containing(CassandraPersistentProperty property, Object bindableValue) { - switch (part.shouldIgnoreCase()) { - case NEVER: - return true; - case WHEN_POSSIBLE: - return part.getProperty().getType() != String.class; - case ALWAYS: - return false; - default: - return true; - } - } - - /** - * Returns the next element from the given {@link Iterator} expecting it to be of a certain type. - * - * @param - * @param iterator - * @param type - * @throws IllegalArgumentException in case the next element in the iterator is not of the given type. - * @return - */ - @SuppressWarnings("unchecked") - private T nextAs(Iterator iterator, Class type) { - Object parameter = iterator.next(); - if (parameter.getClass().isAssignableFrom(type)) { - return (T) parameter; + if (property.isCollectionLike() || ClassUtils.isAssignable(Map.class, property.getType())) { + return QueryBuilder.contains(columnName(property), bindableValue); } - throw new IllegalArgumentException(String.format("Expected parameter type of %s but got %s!", type, - parameter.getClass())); + return QueryBuilder.like(columnName(property), like(Type.CONTAINING, bindableValue)); } - private Object[] nextAsArray(Iterator iterator, CassandraPersistentProperty property) { - Object next = iterator.next(); // TODO nextConverted(property); + private Object like(Type type, Object value) { + + if (value == null) { + return null; + } + + if (type == Type.LIKE) { + return value; + } + + if (type == Type.CONTAINING) { + return "%" + value + "%"; + } + + if (type == Type.STARTING_WITH) { + return value + "%"; + } + + if (type == Type.ENDING_WITH) { + return "%" + value; + } + + throw new IllegalArgumentException(String.format("Part Type [%s] not supported with like queries", type)); + + } + + private static String columnName(CassandraPersistentProperty property) { + return property.getColumnName().toCql(); + } + + private Object[] nextAsArray(CassandraPersistentProperty property, PotentiallyConvertingIterator iterator) { + + Object next = iterator.nextConverted(property); if (next instanceof Collection) { return ((Collection) next).toArray(); @@ -217,4 +222,91 @@ class CassandraQueryCreator extends AbstractQueryCreator { return new Object[] { next }; } + + /** + * Where clause builder. Collects {@link Clause clauses} and builds the where-clause depending on the WHERE type. + * + * @author Mark Paluch + */ + static class WhereBuilder { + + private List clauses = new ArrayList(); + + Clause and(Clause clause) { + + clauses.add(clause); + return clause; + } + + Select.Where build(Select.Where where) { + + for (Clause clause : clauses) { + where = where.and(clause); + } + return where; + } + } + + /** + * @author Mark Paluch + */ + static class StatementBuilder { + + /** + * Build a {@link Select} statement from the given {@link WhereBuilder} and {@link Sort}. Resolves property names + * for {@link Sort} using the {@link CassandraPersistentEntity}. + * + * @param whereBuilder + * @param entity + * @param sort + * @return + */ + static Select select(CassandraPersistentEntity entity, WhereBuilder whereBuilder, Sort sort) { + + Select select = QueryBuilder.select().from(entity.getTableName().toCql()); + whereBuilder.build(select.where()); + + if (sort != null) { + + for (Order order : sort) { + + String dotPath = order.getProperty(); + CassandraPersistentProperty property = getPersistentProperty(entity, dotPath); + + if (order.isAscending()) { + select.orderBy(QueryBuilder.asc(columnName(property))); + } else { + select.orderBy(QueryBuilder.desc(columnName(property))); + } + } + } + + return select; + } + + private static CassandraPersistentProperty getPersistentProperty(CassandraPersistentEntity entity, + String dotPath) { + + String[] segments = PUNCTATION_PATTERN.split(dotPath); + + CassandraPersistentProperty property = null; + CassandraPersistentEntity currentEntity = entity; + + for (String segment : segments) { + property = currentEntity.getPersistentProperty(segment); + + if (property != null && property.isCompositePrimaryKey()) { + currentEntity = property.getCompositePrimaryKeyEntity(); + } + } + + if (property != null) { + + return property; + } + + throw new IllegalArgumentException( + String.format("Cannot resolve path [%s] to a property of [%s]", dotPath, entity.getName())); + } + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java index be295f771..b38f07a57 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java @@ -21,6 +21,7 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.repository.Query; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; @@ -28,6 +29,7 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import com.datastax.driver.core.ResultSet; @@ -41,16 +43,9 @@ import com.datastax.driver.core.ResultSet; */ public class CassandraQueryMethod extends QueryMethod { - private boolean queryCached = false; - - @SuppressWarnings("all") - private final CassandraMappingContext mappingContext; - private final Method method; - - private Query query; - - private String queryString; + private final CassandraMappingContext mappingContext; + private CassandraEntityMetadata metadata; /** * Creates a new {@link CassandraQueryMethod} from the given {@link Method}. @@ -85,6 +80,36 @@ public class CassandraQueryMethod extends QueryMethod { } } + @Override + public CassandraEntityMetadata getEntityInformation() { + + if (metadata == null) { + + Class returnedObjectType = getReturnedObjectType(); + Class domainClass = getDomainClass(); + + if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType)) { + + this.metadata = new SimpleCassandraEntityMetadata((Class) domainClass, + mappingContext.getPersistentEntity(domainClass)); + + } else { + + CassandraPersistentEntity returnedEntity = mappingContext.getPersistentEntity(returnedObjectType); + CassandraPersistentEntity managedEntity = mappingContext.getPersistentEntity(domainClass); + returnedEntity = returnedEntity == null || returnedEntity.getType().isInterface() ? managedEntity + : returnedEntity; + CassandraPersistentEntity collectionEntity = domainClass.isAssignableFrom(returnedObjectType) + ? returnedEntity : managedEntity; + + this.metadata = new SimpleCassandraEntityMetadata((Class) returnedEntity.getType(), + collectionEntity); + } + } + + return this.metadata; + } + /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method) */ @@ -93,18 +118,6 @@ public class CassandraQueryMethod extends QueryMethod { return new CassandraParameters(method); } - /** - * Returns the {@link Query} annotation that is applied to the method or {@code null} if none available. - */ - Query getQueryAnnotation() { - if (query == null) { - query = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); - queryCached = true; - } - - return query; - } - /** * Returns whether the method has an annotated query. */ @@ -115,15 +128,22 @@ public class CassandraQueryMethod extends QueryMethod { /** * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found * nor the attribute was specified. + * + * @return */ public String getAnnotatedQuery() { - if (!queryCached) { - queryString = (String) AnnotationUtils.getValue(getQueryAnnotation()); - queryString = (StringUtils.hasText(queryString) ? queryString : null); - } + String query = (String) AnnotationUtils.getValue(getQueryAnnotation()); + return StringUtils.hasText(query) ? query : null; + } - return queryString; + /** + * Returns the {@link Query} annotation that is applied to the method or {@code null} if none available. + * + * @return + */ + Query getQueryAnnotation() { + return AnnotatedElementUtils.findMergedAnnotation(method, Query.class); } /** diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java index 17c0968b6..a288019e5 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java @@ -15,14 +15,26 @@ */ package org.springframework.data.cassandra.repository.query; +import java.util.Collection; import java.util.Iterator; +import java.util.Set; +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.CustomConversions; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder; +import org.springframework.data.cassandra.mapping.CassandraType; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import com.datastax.driver.core.CodecRegistry; import com.datastax.driver.core.DataType; +import com.datastax.driver.core.DataType.CollectionType; import com.datastax.driver.core.TypeCodec; /** @@ -34,10 +46,12 @@ import com.datastax.driver.core.TypeCodec; */ class ConvertingParameterAccessor implements CassandraParameterAccessor { + private final static TypeInformation SET = ClassTypeInformation.from(Set.class); + private final CassandraConverter cassandraConverter; private final CassandraParameterAccessor delegate; - public ConvertingParameterAccessor(CassandraConverter cassandraConverter, CassandraParameterAccessor delegate) { + ConvertingParameterAccessor(CassandraConverter cassandraConverter, CassandraParameterAccessor delegate) { this.cassandraConverter = cassandraConverter; this.delegate = delegate; @@ -72,7 +86,12 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { */ @Override public Object getBindableValue(int index) { - return potentiallyConvert(index, delegate.getBindableValue(index)); + return potentiallyConvert(index, delegate.getBindableValue(index), null); + } + + @Override + public CassandraType findCassandraType(int index) { + return delegate.findCassandraType(index); } /* (non-Javadoc) @@ -80,7 +99,14 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { */ @Override public DataType getDataType(int index) { - return delegate.getDataType(index); + + DataType dataType = delegate.getDataType(index); + + if (dataType != null) { + return dataType; + } + + return cassandraConverter.getMappingContext().getDataType(getParameterType(index)); } /* (non-Javadoc) @@ -99,11 +125,6 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return delegate.hasBindableNullValue(); } - /* - * (non-Javadoc) - * - * @see java.lang.Iterable#iterator() - */ /* (non-Javadoc) * @see org.springframework.data.repository.query.ParameterAccessor#iterator() */ @@ -111,20 +132,37 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return new ConvertingIterator(delegate.iterator()); } - private Object potentiallyConvert(int index, Object bindableValue) { + @SuppressWarnings("unchecked") + private Object potentiallyConvert(int index, Object bindableValue, CassandraPersistentProperty property) { if (bindableValue == null) { return null; } - DataType parameterType = getDataType(index); - - if (parameterType == null) { - parameterType = cassandraConverter.getMappingContext().getDataType(getParameterType(index)); + if (bindableValue.getClass().isArray()) { + return bindableValue; } + DataType parameterType = getDataType(index, property); TypeCodec cassandraType = CodecRegistry.DEFAULT_INSTANCE.codecFor(parameterType); + if (property != null && getCustomConversions().hasCustomWriteTarget(property.getActualType()) + && property.isCollectionLike()) { + + Class customWriteTarget = getCustomConversions().getCustomWriteTarget(property.getActualType()); + if (Collection.class.isAssignableFrom(property.getType()) && bindableValue instanceof Collection) { + + Collection original = (Collection) bindableValue; + Collection converted = CollectionFactory.createCollection(property.getType(), original.size()); + + for (Object o : original) { + converted.add(getConversionService().convert(o, customWriteTarget)); + } + + return converted; + } + } + if (cassandraType.getJavaType().getRawType().isAssignableFrom(bindableValue.getClass())) { return bindableValue; } @@ -132,12 +170,79 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return cassandraConverter.getConversionService().convert(bindableValue, cassandraType.getJavaType().getRawType()); } + private CustomConversions getCustomConversions() { + return cassandraConverter.getCustomConversions(); + } + + private ConversionService getConversionService() { + return cassandraConverter.getConversionService(); + } + + /** + * Return the {@link DataType} based on annotated parameters with {@link CassandraType}, the + * {@link CassandraPersistentProperty} type or the declared parameter type. + * + * @param index + * @param cassandraPersistentProperty + * @return the {@link DataType} + */ + DataType getDataType(int index, CassandraPersistentProperty cassandraPersistentProperty) { + + CassandraType cassandraType = delegate.findCassandraType(index); + + if (cassandraType != null) { + return CassandraSimpleTypeHolder.getDataTypeFor(cassandraType.type()); + } + + CassandraMappingContext mappingContext = cassandraConverter.getMappingContext(); + TypeInformation typeInformation = ClassTypeInformation.from(getParameterType(index)); + + if (cassandraPersistentProperty == null) { + return mappingContext.getDataType(typeInformation.getType()); + } + + DataType dataType = mappingContext.getDataType(cassandraPersistentProperty); + + if (cassandraPersistentProperty.isCollectionLike() && !typeInformation.isCollectionLike()) { + + if (dataType instanceof CollectionType) { + + CollectionType collectionType = (CollectionType) dataType; + if (collectionType.getTypeArguments().size() == 1) { + return collectionType.getTypeArguments().get(0); + } + } + } + + if (!cassandraPersistentProperty.isCollectionLike() && typeInformation.isCollectionLike()) { + + if (typeInformation.isAssignableFrom(SET)) { + return DataType.set(dataType); + } + + return DataType.list(dataType); + } + + if (cassandraPersistentProperty.isMap()) { + + if (dataType instanceof CollectionType) { + + CollectionType collectionType = (CollectionType) dataType; + if (collectionType.getTypeArguments().size() == 2) { + return collectionType.getTypeArguments().get(0); + } + } + } + + return mappingContext.getDataType(cassandraPersistentProperty); + } + /** * Custom {@link Iterator} to convert items before returning them. * * @author Mark Paluch */ - private class ConvertingIterator implements Iterator { + private class ConvertingIterator implements PotentiallyConvertingIterator { private final Iterator delegate; private int index = 0; @@ -145,9 +250,9 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { /** * Creates a new {@link ConvertingIterator} for the given delegate. * - * @param delegate + * @param delegate must not be {@literal null}. */ - public ConvertingIterator(Iterator delegate) { + ConvertingIterator(Iterator delegate) { this.delegate = delegate; } @@ -164,7 +269,7 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { * @see java.util.Iterator#next() */ public Object next() { - return potentiallyConvert(index++, next()); + return potentiallyConvert(index++, delegate.next(), null); } /* @@ -174,5 +279,25 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { public void remove() { delegate.remove(); } + + @Override + public Object nextConverted(CassandraPersistentProperty property) { + return potentiallyConvert(index++, delegate.next(), property); + } + } + + /** + * Custom {@link Iterator} that adds a method to access elements in a converted manner. + * + * @author Mark Paluch + */ + interface PotentiallyConvertingIterator extends Iterator { + + /** + * Returns the next element and pass in type information for potential conversion. + * + * @return the converted object, may be {@literal null}. + */ + Object nextConverted(CassandraPersistentProperty property); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java index 9b4439951..f1106a52d 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2014-2016 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. @@ -24,6 +24,9 @@ import org.springframework.data.repository.query.parser.PartTree; /** * {@link RepositoryQuery} implementation for Cassandra. + * + * @author Matthew Adams + * @author Mark Paluch */ public class PartTreeCassandraQuery extends AbstractCassandraQuery { @@ -34,13 +37,14 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { * Creates a new {@link PartTreeCassandraQuery} from the given {@link QueryMethod} and {@link CassandraTemplate}. * * @param method must not be {@literal null}. - * @param template must not be {@literal null}. + * @param operations must not be {@literal null}. */ - public PartTreeCassandraQuery(CassandraQueryMethod method, CassandraOperations cassandraOperations) { + public PartTreeCassandraQuery(CassandraQueryMethod method, CassandraOperations operations) { + + super(method, operations); - super(method, cassandraOperations); this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); - this.context = cassandraOperations.getConverter().getMappingContext(); + this.context = operations.getConverter().getMappingContext(); } /** @@ -52,10 +56,15 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery { return tree; } + /* + * (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor, boolean) + */ @Override protected String createQuery(CassandraParameterAccessor accessor) { - CassandraQueryCreator creator = new CassandraQueryCreator(tree, accessor, context); - return creator.createQuery().getQueryString(); + CassandraQueryCreator creator = new CassandraQueryCreator(tree, accessor, context, + getQueryMethod().getEntityInformation()); + return creator.createQuery().toString(); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/SimpleCassandraEntityMetadata.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/SimpleCassandraEntityMetadata.java new file mode 100644 index 000000000..906714590 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/SimpleCassandraEntityMetadata.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.util.Assert; + +/** + * Implementation of {@link CassandraEntityMetadata} based on the type and {@link CassandraPersistentEntity}. + * + * @author Mark Paluch + * @since 1.5 + */ +class SimpleCassandraEntityMetadata implements CassandraEntityMetadata { + + private final Class type; + private final CassandraPersistentEntity tableEntity; + + /** + * Creates a new {@link SimpleCassandraEntityMetadata} using the given type and {@link CassandraPersistentEntity} to + * use for table lookups. + * + * @param type must not be {@literal null}. + * @param tableEntity must not be {@literal null} or empty. + */ + public SimpleCassandraEntityMetadata(Class type, CassandraPersistentEntity tableEntity) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(tableEntity, "Collection entity must not be null or empty!"); + + this.type = type; + this.tableEntity = tableEntity; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.CassandraEntityMetadata#getTableName() + */ + @Override + public CqlIdentifier getTableName() { + return tableEntity.getTableName(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.core.EntityMetadata#getJavaType() + */ + @Override + public Class getJavaType() { + return type; + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java index 15959297c..bf9a7e538 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors + * Copyright 2013-2016 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. @@ -18,13 +18,13 @@ package org.springframework.data.cassandra.repository.support; import java.io.Serializable; import java.lang.reflect.Method; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.repository.TypedIdCassandraRepository; import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; import org.springframework.data.cassandra.repository.query.CassandraQueryMethod; +import org.springframework.data.cassandra.repository.query.PartTreeCassandraQuery; import org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery; import org.springframework.data.mapping.model.MappingException; import org.springframework.data.projection.ProjectionFactory; @@ -43,8 +43,8 @@ import org.springframework.util.Assert; * @author Alex Shvid * @author Matthew T. Adams * @author Thomas Darimont + * @author Mark Paluch */ - public class CassandraRepositoryFactory extends RepositoryFactorySupport { private final CassandraOperations cassandraOperations; @@ -61,16 +61,19 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { this.cassandraOperations = cassandraOperations; this.mappingContext = cassandraOperations.getConverter().getMappingContext(); - - // TODO: remove when supporting declarative query methods - setQueryLookupStrategyKey(QueryLookupStrategy.Key.USE_DECLARED_QUERY); } + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata) + */ @Override protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleCassandraRepository.class; } + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryInformation) + */ @Override protected Object getTargetRepository(RepositoryInformation information) { @@ -78,6 +81,9 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { return getTargetRepositoryViaReflection(information, entityInformation, cassandraOperations); } + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class) + */ @Override @SuppressWarnings("unchecked") public CassandraEntityInformation getEntityInformation(Class domainClass) { @@ -85,14 +91,17 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { CassandraPersistentEntity entity = mappingContext.getPersistentEntity(domainClass); if (entity == null) { - throw new MappingException(String.format("Could not lookup mapping metadata for domain class %s!", - domainClass.getName())); + throw new MappingException( + String.format("Could not lookup mapping metadata for domain class %s!", domainClass.getName())); } return new MappingCassandraEntityInformation((CassandraPersistentEntity) entity, cassandraOperations.getConverter()); } + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key) + */ @Override protected QueryLookupStrategy getQueryLookupStrategy(Key key) { return new CassandraQueryLookupStrategy(); @@ -117,7 +126,7 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { } else if (queryMethod.hasAnnotatedQuery()) { return new StringBasedCassandraQuery(queryMethod, cassandraOperations); } else { - throw new InvalidDataAccessApiUsageException("declarative query methods are a todo"); + return new PartTreeCassandraQuery(queryMethod, cassandraOperations); } } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java index 92eccf73f..706ec31f5 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java @@ -29,4 +29,5 @@ public class Person { @Id String id; String firstname; + String lastname; } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java index 965eeed6a..aea5da220 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java @@ -44,12 +44,12 @@ public class CassandraParametersUnitTests { * @see DATACASS-296 */ @Test - public void shouldReturnDataTypeForSimpleType() throws Exception { + public void shouldUnknownDataTypeForSimpleType() throws Exception { Method method = PersonRepository.class.getMethod("findByFirstname", String.class); CassandraParameters cassandraParameters = new CassandraParameters(method); - assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(DataType.varchar())); + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(nullValue())); } /** @@ -61,7 +61,8 @@ public class CassandraParametersUnitTests { Method method = PersonRepository.class.getMethod("findByFirstTime", String.class); CassandraParameters cassandraParameters = new CassandraParameters(method); - assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(DataType.time())); + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(notNullValue())); + assertThat(cassandraParameters.getParameter(0).getCassandraType().type(), is(Name.TIME)); } /** @@ -85,7 +86,8 @@ public class CassandraParametersUnitTests { Method method = PersonRepository.class.getMethod("findByAnnotatedObject", Object.class); CassandraParameters cassandraParameters = new CassandraParameters(method); - assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(DataType.time())); + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(notNullValue())); + assertThat(cassandraParameters.getParameter(0).getCassandraType().type(), is(Name.TIME)); } interface PersonRepository { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java new file mode 100644 index 000000000..906f7a4ce --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java @@ -0,0 +1,408 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.cassandra.repository.query.StubParameterAccessor.*; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.cassandra.core.PrimaryKeyType; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.Column; +import org.springframework.data.cassandra.mapping.PrimaryKey; +import org.springframework.data.cassandra.mapping.PrimaryKeyClass; +import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; +import org.springframework.data.cassandra.mapping.Table; +import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * Unit tests for {@link CassandraQueryCreator}. + * + * @author Mark Paluch + * @soundtrack Odyssey - Everybody Move 9Club Mix + */ +public class CassandraQueryCreatorUnitTests { + + CassandraMappingContext context; + CassandraConverter converter; + + @Rule public ExpectedException expection = ExpectedException.none(); + + @Before + public void setUp() throws SecurityException, NoSuchMethodException { + + context = new BasicCassandraMappingContext(); + converter = new MappingCassandraConverter(context); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsQueryCorrectly() { + + String query = createQuery("findByFirstname", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsQueryWithSortCorrectly() { + + String query = createQuery("findByFirstnameOrderByLastname", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname='Walter' ORDER BY lastname ASC;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsAndQueryCorrectly() { + + String query = createQuery("findByFirstnameAndLastname", Person.class, "Walter", "White"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname='Walter' AND lastname='White';"))); + } + + /** + * @see DATACASS-7 + */ + @Test(expected = InvalidDataAccessApiUsageException.class) + public void rejectsNegatingQueryQuery() { + createQuery("findByFirstnameNot", Person.class, "Walter"); + } + + /** + * @see DATACASS-7 + */ + @Test(expected = InvalidDataAccessApiUsageException.class) + public void rejectsOrQuery() { + createQuery("findByFirstnameOrLastname", Person.class, "Walter", "White"); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsGreaterThanQueryCorrectly() { + + String query = createQuery("findByFirstnameGreaterThan", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname>'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsGreaterThanEqualQueryCorrectly() { + + String query = createQuery("findByFirstnameGreaterThanEqual", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname>='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsLessThanQueryCorrectly() { + + String query = createQuery("findByFirstnameLessThan", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname<'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsLessThanEqualQueryCorrectly() { + + String query = createQuery("findByFirstnameLessThanEqual", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname<='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsInQueryCorrectly() { + + String query = createQuery("findByFirstnameIn", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname IN ('Walter');"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsInQueryWithListCorrectly() { + + String query = createQuery("findByFirstnameIn", Person.class, Arrays.asList("Walter", "Gus")); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname IN ('Walter','Gus');"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsInQueryWithArrayCorrectly() { + + String query = createQuery("findByFirstnameInAndLastname", Person.class, new String[] { "Walter", "Gus" }, "Fring"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname IN ('Walter','Gus') AND lastname='Fring';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsLikeQueryCorrectly() { + + assertThat(createQuery("findByFirstnameLike", Person.class, "Wal%ter"), + is(equalTo("SELECT * FROM person WHERE firstname LIKE 'Wal%ter';"))); + assertThat(createQuery("findByFirstnameLike", Person.class, "Walter"), + is(equalTo("SELECT * FROM person WHERE firstname LIKE 'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsStartsWithQueryCorrectly() { + + String query = createQuery("findByFirstnameStartsWith", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname LIKE 'Walter%';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsEndsWithQueryCorrectly() { + + String query = createQuery("findByFirstnameEndsWith", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname LIKE '%Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsContainsQueryOnSimplePropertyCorrectly() { + + String query = createQuery("findByFirstnameContains", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname LIKE '%Walter%';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsContainsQueryOnSetPropertyCorrectly() { + + String query = createQuery("findByMysetContains", TypeWithSet.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithset WHERE myset CONTAINS 'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsContainsQueryOnListPropertyCorrectly() { + + String query = createQuery("findByMylistContains", TypeWithList.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithlist WHERE mylist CONTAINS 'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsContainsQueryOnMapPropertyCorrectly() { + + String query = createQuery("findByMymapContains", TypeWithMap.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithmap WHERE mymap CONTAINS 'Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsIsTrueQueryCorrectly() { + + String query = createQuery("findByFirstnameIsTrue", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname=true;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsIsFalseQueryCorrectly() { + + String query = createQuery("findByFirstnameIsFalse", Person.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname=false;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsQueryUsingQuotingCorrectly() { + + String query = createQuery("findByIdAndSet", QuotedType.class, "Walter", "White"); + + assertThat(query, is(equalTo("SELECT * FROM \"myTable\" WHERE \"my_id\"='Walter' AND \"set\"='White';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsFindByPrimaryKeyPartCorrectly() { + + String query = createQuery("findByKeyFirstname", TypeWithCompositeId.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithcompositeid WHERE firstname='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsFindByPrimaryKeyPartWithSortCorrectly() { + + String query = createQuery("findByKeyFirstnameOrderByKeyLastnameAsc", TypeWithCompositeId.class, "Walter"); + + assertThat(query, is(equalTo("SELECT * FROM typewithcompositeid WHERE firstname='Walter' ORDER BY lastname ASC;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsFindByPrimaryKeyPartOfPrimaryKeyClassCorrectly() { + + String query = createQuery("findByFirstname", Key.class, "Walter"); + + // ⊙_ʘ rly? ヾ( •́д•̀ ;)ノ + assertThat(query, is(equalTo("SELECT * FROM key WHERE firstname='Walter';"))); + } + + /** + * @see DATACASS-7 + */ + @Test(expected = IllegalStateException.class) + public void createsFindByPrimaryKey2PartCorrectly() { + createQuery("findByKey", TypeWithCompositeId.class, new Key()); + } + + private String createQuery(String source, Class entityClass, Object... values) { + + PartTree tree = new PartTree(source, entityClass); + CassandraQueryCreator creator = new CassandraQueryCreator(tree, getAccessor(converter, values), context, + getEntityInformation(entityClass)); + return creator.createQuery().toString(); + } + + private EntityMetadata getEntityInformation(final Class entityClass) { + return new EntityMetadata() { + @Override + public Class getJavaType() { + return entityClass; + } + }; + } + + @Table + private static class TypeWithSet { + + @Id String id; + Set myset; + } + + @Table + private static class TypeWithList { + + @Id String id; + List mylist; + } + + @Table + private static class TypeWithMap { + + @Id String id; + Map mymap; + } + + @Table(value = "myTable", forceQuote = true) + private static class QuotedType { + + @PrimaryKey(value = "my_id", forceQuote = true) String id; + + @Column(value = "set") Set set; + } + + @PrimaryKeyClass + private static class Key implements Serializable { + + @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 1) String firstname; + + @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 1) String lastname; + } + + @Table + private static class TypeWithCompositeId { + + @PrimaryKey Key key; + + String city; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethodUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethodUnitTests.java new file mode 100644 index 000000000..1378bbc26 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethodUnitTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * Unit tests for {@link CassandraQueryMethod}. + * + * @author Mark Paluch + */ +public class CassandraQueryMethodUnitTests { + + CassandraMappingContext context; + + @Before + public void setUp() { + context = new BasicCassandraMappingContext(); + } + + /** + * @see DATACASS-7 + */ + @Test + public void detectsCollectionFromRepoTypeIfReturnTypeNotAssignable() throws Exception { + + CassandraQueryMethod queryMethod = queryMethod(SampleRepository.class, "method"); + CassandraEntityMetadata metadata = queryMethod.getEntityInformation(); + + assertThat(metadata.getJavaType(), is(typeCompatibleWith(Person.class))); + assertThat(metadata.getTableName().toCql(), is("person")); + } + + /** + * @see DATACASS-7 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullMappingContext() throws Exception { + + Method method = SampleRepository.class.getMethod("method"); + + new CassandraQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), + new SpelAwareProxyProjectionFactory(), null); + } + + /** + * @see DATACASS-7 + */ + @Test + public void considersMethodAsCollectionQuery() throws Exception { + + CassandraQueryMethod queryMethod = queryMethod(SampleRepository.class, "method"); + + assertThat(queryMethod.isCollectionQuery(), is(true)); + } + + private CassandraQueryMethod queryMethod(Class repository, String name, Class... parameters) throws Exception { + + Method method = repository.getMethod(name, parameters); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + return new CassandraQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context); + } + + interface SampleRepository extends Repository { + + List method(); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java index 649cddf53..d4c0c1940 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java @@ -20,6 +20,10 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import org.junit.Before; import org.junit.Test; @@ -28,6 +32,9 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.cassandra.repository.query.ConvertingParameterAccessor.PotentiallyConvertingIterator; import com.datastax.driver.core.DataType; @@ -41,14 +48,16 @@ import com.datastax.driver.core.DataType; public class ConvertingParameterAccessorUnitTests { @Mock CassandraParameterAccessor delegateMock; - + @Mock CassandraPersistentProperty propertyMock; MappingCassandraConverter converter; + ConvertingParameterAccessor accessor; @Before public void setUp() { this.converter = new MappingCassandraConverter(new BasicCassandraMappingContext()); this.converter.afterPropertiesSet(); + this.accessor = new ConvertingParameterAccessor(converter, delegateMock); } /** @@ -56,9 +65,6 @@ public class ConvertingParameterAccessorUnitTests { */ @Test public void shouldReturnNullBindableValue() { - - ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, delegateMock); - assertThat(accessor.getBindableValue(0), is(nullValue())); } @@ -66,12 +72,12 @@ public class ConvertingParameterAccessorUnitTests { * @see DATACASS-296 */ @Test + @SuppressWarnings({"rawtypes", "unchecked"}) public void shouldReturnNativeBindableValue() { - ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, delegateMock); - when(delegateMock.getBindableValue(0)).thenReturn("hello"); when(delegateMock.getDataType(0)).thenReturn(DataType.varchar()); + when(delegateMock.getParameterType(0)).thenReturn((Class) String.class); assertThat(accessor.getBindableValue(0), is(equalTo((Object) "hello"))); } @@ -80,11 +86,9 @@ public class ConvertingParameterAccessorUnitTests { * @see DATACASS-296 */ @Test - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "unchecked"}) public void shouldReturnConvertedBindableValue() { - ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, delegateMock); - LocalDate localDate = LocalDate.of(2010, 7, 4); when(delegateMock.getBindableValue(0)).thenReturn(localDate); @@ -93,4 +97,70 @@ public class ConvertingParameterAccessorUnitTests { assertThat(accessor.getBindableValue(0), is(equalTo((Object) com.datastax.driver.core.LocalDate.fromYearMonthDay(2010, 7, 4)))); } + + /** + * @see DATACASS-296 + * @see DATACASS-7 + */ + @Test + public void shouldReturnDataTypeProvidedByDelegate() { + + when(delegateMock.getDataType(0)).thenReturn(DataType.varchar()); + + assertThat(accessor.getDataType(0), is(equalTo(DataType.varchar()))); + } + + /** + * @see DATACASS-296 + * @see DATACASS-7 + */ + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void shouldConvertCollections() { + + LocalDate localDate = LocalDate.of(2010, 7, 4); + + when(delegateMock.iterator()).thenReturn((Iterator) Arrays.asList(Collections.singletonList(localDate)).iterator()); + when(delegateMock.getDataType(0)).thenReturn(DataType.list(DataType.date())); + when(delegateMock.getParameterType(0)).thenReturn((Class) List.class); + when(propertyMock.getType()).thenReturn((Class) List.class); + when(propertyMock.getActualType()).thenReturn((Class) LocalDate.class); + when(propertyMock.isCollectionLike()).thenReturn(true); + + PotentiallyConvertingIterator iterator = (PotentiallyConvertingIterator) accessor.iterator(); + Object converted = iterator.nextConverted(propertyMock); + + assertThat(converted, is(instanceOf(List.class))); + + List list = (List) converted; + assertThat(list.get(0), is(instanceOf(com.datastax.driver.core.LocalDate.class))); + } + + /** + * @see DATACASS-7 + */ + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void shouldProvideTypeBasedOnValue() { + + when(delegateMock.getDataType(0)).thenReturn(null); + when(delegateMock.getParameterType(0)).thenReturn((Class) LocalDate.class); + + assertThat(accessor.getDataType(0), is(equalTo(DataType.date()))); + } + + /** + * @see DATACASS-7 + */ + @Test + @SuppressWarnings("rawtypes") + public void shouldProvideTypeBasedOnPropertyType() { + + when(propertyMock.getDataType()).thenReturn(DataType.varchar()); + when(propertyMock.findAnnotation(CassandraType.class)).thenReturn(mock(CassandraType.class)); + when(delegateMock.getParameterType(0)).thenReturn((Class) String.class); + when(delegateMock.getDataType(0)).thenReturn(null); + + assertThat(accessor.getDataType(0, propertyMock), is(equalTo(DataType.varchar()))); + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java new file mode 100644 index 000000000..2bd5fc542 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * Unit tests for {@link PartTreeCassandraQuery}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class PartTreeCassandraQueryUnitTests { + + public @Rule ExpectedException exception = ExpectedException.none(); + + @Mock CassandraOperations cassandraOperationsMock; + + CassandraMappingContext mappingContext; + CassandraConverter converter; + + @Before + public void setUp() { + + mappingContext = new BasicCassandraMappingContext(); + converter = new MappingCassandraConverter(mappingContext); + when(cassandraOperationsMock.getConverter()).thenReturn(converter); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldDeriveSimpleQuery() { + + String query = deriveQueryFromMethod("findByLastname", "foo"); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE lastname='foo';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldDeriveSimpleQueryWithoutNames() { + + String query = deriveQueryFromMethod("findPersonBy"); + + assertThat(query, is(equalTo("SELECT * FROM person;"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldDeriveAndQuery() { + + String query = deriveQueryFromMethod("findByFirstnameAndLastname", "foo", "bar" ); + + assertThat(query, is(equalTo("SELECT * FROM person WHERE firstname='foo' AND lastname='bar';"))); + } + + /** + * @see DATACASS-7 + */ + @Test + public void usesDynamicProjection() { + + String query = deriveQueryFromMethod("findDynamicallyProjectedBy", PersonProjection.class); + + assertThat(query, is(equalTo("SELECT * FROM person;"))); + } + + + private String deriveQueryFromMethod(String method, Object... args) { + + Class[] types = new Class[args.length]; + + for (int i = 0; i < args.length; i++) { + types[i] = args[i].getClass(); + } + + PartTreeCassandraQuery partTreeQuery = createQueryForMethod(method, types); + + CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(partTreeQuery.getQueryMethod(), args); + return partTreeQuery.createQuery(new ConvertingParameterAccessor(cassandraOperationsMock.getConverter(), accessor)); + } + + private PartTreeCassandraQuery createQueryForMethod(String methodName, Class... paramTypes) { + + try { + + Method method = Repo.class.getMethod(methodName, paramTypes); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, new DefaultRepositoryMetadata(Repo.class), factory, + mappingContext); + + return new PartTreeCassandraQuery(queryMethod, cassandraOperationsMock); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } catch (SecurityException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + @SuppressWarnings("unused") + interface Repo extends CassandraRepository { + + @Query() + Person findByLastname(String lastname); + + Person findByFirstnameAndLastname(String firstname, String lastname); + + Person findPersonByFirstnameAndLastname(String firstname, String lastname); + + Person findByAge(Integer age); + + Person findPersonBy(); + + PersonProjection findPersonProjectedBy(); + + T findDynamicallyProjectedBy(Class type); + + } + + interface PersonProjection { + + String getFirstname(); + + String getLastname(); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java new file mode 100644 index 000000000..75abb515d --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java @@ -0,0 +1,102 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import java.util.Arrays; +import java.util.Iterator; + +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Distance; +import org.springframework.data.repository.query.ParameterAccessor; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.DataType; + +/** + * Simple {@link ParameterAccessor} that returns the given parameters unfiltered. + * + * @author Mark Paluch + */ +class StubParameterAccessor implements CassandraParameterAccessor { + + private final Object[] values; + + /** + * Creates a new {@link ConvertingParameterAccessor} backed by a {@link StubParameterAccessor} simply returning the + * given parameters converted but unfiltered. + * + * @param converter + * @param parameters + * @return + */ + public static ConvertingParameterAccessor getAccessor(CassandraConverter converter, Object... parameters) { + return new ConvertingParameterAccessor(converter, new StubParameterAccessor(parameters)); + } + + @SuppressWarnings("unchecked") + public StubParameterAccessor(Object... values) { + this.values = values; + } + + @Override + public DataType getDataType(int index) { + return CodecRegistry.DEFAULT_INSTANCE.codecFor(values[index]).getCqlType(); + } + + @Override + public Class getParameterType(int index) { + return values[index].getClass(); + } + + @Override + public Pageable getPageable() { + return null; + } + + @Override + public Sort getSort() { + return null; + } + + @Override + public Class getDynamicProjection() { + return null; + } + + @Override + public Object getBindableValue(int index) { + return values[index]; + } + + @Override + public boolean hasBindableNullValue() { + return false; + } + + @Override + public Iterator iterator() { + return Arrays.asList(values).iterator(); + } + + @Override + public CassandraType findCassandraType(int index) { + return null; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryUnitTests.java new file mode 100644 index 000000000..29842dc2e --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactoryUnitTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.Serializable; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; +import org.springframework.data.repository.Repository; + +/** + * Unit tests for {@link CassandraRepositoryFactory}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class CassandraRepositoryFactoryUnitTests { + + @Mock CassandraTemplate template; + @Mock CassandraConverter converter; + @Mock CassandraMappingContext mappingContext; + @Mock CassandraPersistentEntity entity; + + @Before + public void setUp() { + + when(template.getConverter()).thenReturn(converter); + when(converter.getMappingContext()).thenReturn(mappingContext); + } + + /** + * @see DATACASS-7 + */ + @Test + public void usesMappingCassandraEntityInformationIfMappingContextSet() { + + when(mappingContext.getPersistentEntity(Person.class)).thenReturn(entity); + when(entity.getType()).thenReturn(Person.class); + + CassandraRepositoryFactory factory = new CassandraRepositoryFactory(template); + CassandraEntityInformation entityInformation = factory.getEntityInformation(Person.class); + assertTrue(entityInformation instanceof MappingCassandraEntityInformation); + } + + /** + * @see DATACASS-7 + */ + @Test + public void createsRepositoryWithIdTypeLong() { + + when(mappingContext.getPersistentEntity(Person.class)).thenReturn(entity); + when(entity.getType()).thenReturn(Person.class); + + CassandraRepositoryFactory factory = new CassandraRepositoryFactory(template); + MyPersonRepository repository = factory.getRepository(MyPersonRepository.class); + assertThat(repository, is(notNullValue())); + } + + interface MyPersonRepository extends Repository { + + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/AsynchronousCassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/AsynchronousCassandraTemplateIntegrationTests.java index 3057a97e4..5dda280cc 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/AsynchronousCassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/AsynchronousCassandraTemplateIntegrationTests.java @@ -246,7 +246,7 @@ public class AsynchronousCassandraTemplateIntegrationTests extends AbstractSprin @AllArgsConstructor @NoArgsConstructor @SuppressWarnings("unused") - public static class Person { + static class Person { @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) String id; @Column String firstname; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java index be1f9170f..aba473d43 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java @@ -734,7 +734,11 @@ public class CassandraOperationsIntegrationTests extends AbstractSpringDataEmbed * @see DATACASS-182 */ @Test - public void stream() { + public void stream() throws InterruptedException { + + while(template.select("SELECT * FROM book", Book.class).size() != 0){ + Thread.sleep(10); + } template.insert(getBookList(20)); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java new file mode 100644 index 000000000..933d97db5 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Address.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.test.integration.repository.querymethods.conversion; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Mark Paluch + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +class Address { + + String city; + String country; +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java new file mode 100644 index 000000000..a646a8c0f --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/Contact.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.test.integration.repository.querymethods.conversion; + +import java.util.List; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.Table; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author Mark Paluch + */ +@Table +@Data +@NoArgsConstructor +class Contact { + + @Id String id; + + Address address; + List
    addresses; + + public Contact(String id) { + this.id = id; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java new file mode 100644 index 000000000..2567ff361 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/conversion/ParameterConversionIntegrationTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.test.integration.repository.querymethods.conversion; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.codehaus.jackson.map.ObjectMapper; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.convert.CustomConversions; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.base.PersonRepository; +import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; +import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.StringUtils; + +/** + * Integration tests for query derivation through {@link PersonRepository}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class ParameterConversionIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { + + @Configuration + @EnableCassandraRepositories(considerNestedRepositories = true) + public static class Config extends IntegrationTestConfig { + + @Override + public String[] getEntityBasePackages() { + return new String[] { Contact.class.getPackage().getName() }; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE_DROP_UNUSED; + } + + @Override + public CustomConversions customConversions() { + return new CustomConversions(Arrays.asList(AddressReadConverter.INSTANCE, AddressWriteConverter.INSTANCE)); + } + } + + @Autowired CassandraOperations template; + @Autowired ContactRepository contactRepository; + + Contact walter, flynn; + + @Before + public void before() { + + deleteAllEntities(); + + template.execute("CREATE INDEX IF NOT EXISTS contact_address ON contact (address);"); + template.execute("CREATE INDEX IF NOT EXISTS contact_addresses ON contact (addresses);"); + + walter = new Contact("Walter"); + walter.setAddress(new Address("Albuquerque", "USA")); + walter.setAddresses(Arrays.asList(new Address("Albuquerque", "USA"), new Address("New Hampshire", "USA"), + new Address("Grocery Store", "Mexico"))); + + flynn = new Contact("Flynn"); + flynn.setAddress(new Address("Albuquerque", "USA")); + flynn.setAddresses(Collections.singletonList(new Address("Albuquerque", "USA"))); + + walter = contactRepository.save(walter); + flynn = contactRepository.save(flynn); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByConvertedParameter() { + + List contacts = contactRepository.findByAddress(walter.getAddress()); + + assertThat(contacts, hasItems(walter, flynn)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByStringParameter() { + + String parameter = AddressWriteConverter.INSTANCE.convert(walter.getAddress()); + List contacts = contactRepository.findByAddress(parameter); + + assertThat(contacts, hasItems(walter, flynn)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void findByAddressesIn() { + + assertThat(contactRepository.findByAddressesContains(flynn.address), containsInAnyOrder(flynn, walter)); + assertThat(contactRepository.findByAddressesContains(walter.addresses.get(1)), contains(walter)); + } + + interface ContactRepository extends CassandraRepository { + + List findByAddress(Address address); + + List findByAddress(String address); + + List findByAddressesContains(Address address); + } + + /** + * @author Mark Paluch + */ + static enum AddressReadConverter implements Converter { + + INSTANCE; + + public Address convert(String source) { + + if (StringUtils.hasText(source)) { + try { + return new ObjectMapper().readValue(source, Address.class); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + return null; + } + } + + /** + * @author Mark Paluch + */ + static enum AddressWriteConverter implements Converter { + INSTANCE; + + public String convert(Address source) { + + try { + return new ObjectMapper().writeValueAsString(source); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java index d8c38eae8..73fba9e21 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java @@ -20,17 +20,18 @@ import java.time.ZoneId; import java.util.Date; import org.springframework.cassandra.core.PrimaryKeyType; -import org.springframework.data.cassandra.mapping.Indexed; import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; import org.springframework.data.cassandra.mapping.Table; import lombok.Data; +import lombok.NoArgsConstructor; /** * Sample domain class. */ @Table @Data +@NoArgsConstructor public class Person { @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 0) private String lastname; @@ -44,4 +45,10 @@ public class Person { private LocalDate createdDate; private ZoneId zoneId; + + public Person(String firstname, String lastname) { + + this.firstname = firstname; + this.lastname = lastname; + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java new file mode 100644 index 000000000..b7603fcc1 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.test.integration.repository.querymethods.derived; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; +import org.springframework.data.domain.Sort; + +/** + * @author Mark Paluch + */ +interface PersonRepository extends CassandraRepository { + + List findByLastname(String lastname); + + List findByLastname(String lastname, Sort sort); + + List findByLastnameOrderByFirstnameAsc(String lastname); + + Person findByFirstnameAndLastname(String firstname, String lastname); + + Person findByCreatedDate(LocalDate createdDate); + + Person findByNicknameStartsWith(String prefix); + + Person findByNicknameContains(String contains); + + Person findByNumberOfChildren(NumberOfChildren numberOfChildren); + + Collection findPersonProjectedBy(); + + @Query("select * from person where firstname = ?0 and lastname = 'White'") + List findByFirstname(String firstname); + + enum NumberOfChildren { + ZERO, ONE, TWO, + } + + interface PersonProjection { + + String getFirstname(); + + String getLastname(); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java new file mode 100644 index 000000000..d7ad2bca6 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java @@ -0,0 +1,237 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.test.integration.repository.querymethods.derived; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.junit.Assume.*; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.SpringVersion; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; +import org.springframework.data.cassandra.test.integration.repository.querymethods.derived.PersonRepository.NumberOfChildren; +import org.springframework.data.cassandra.test.integration.repository.querymethods.derived.PersonRepository.PersonProjection; +import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; +import org.springframework.data.cassandra.test.integration.support.CassandraVersion; +import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig; +import org.springframework.data.domain.Sort; +import org.springframework.data.util.Version; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration tests for query derivation through {@link PersonRepository}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { + + @Configuration + @EnableCassandraRepositories + public static class Config extends IntegrationTestConfig { + + @Override + public String[] getEntityBasePackages() { + return new String[] { Person.class.getPackage().getName() }; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE_DROP_UNUSED; + } + + } + + @Autowired CassandraOperations template; + @Autowired PersonRepository personRepository; + + Person walter, skyler, flynn; + + @Before + public void before() { + + deleteAllEntities(); + + Person person = new Person("Walter", "White"); + person.setNumberOfChildren(2); + + walter = personRepository.save(person); + skyler = personRepository.save(new Person("Skyler", "White")); + flynn = personRepository.save(new Person("Flynn (Walter Jr.)", "White")); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByLastname() { + + List result = personRepository.findByLastname("White"); + + assertThat(result, hasItems(walter, skyler, flynn)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByLastnameAndDynamicSort() { + + List result = personRepository.findByLastname("White", new Sort("firstname")); + + assertThat(result, contains(flynn, skyler, walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByLastnameWithOrdering() { + + List result = personRepository.findByLastnameOrderByFirstnameAsc("White"); + + assertThat(result, contains(flynn, skyler, walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByFirstnameAndLastname() { + + Person result = personRepository.findByFirstnameAndLastname("Walter", "White"); + + assertThat(result, is(walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void executesCollectionQueryWithProjectionCorrectly() { + + Collection collection = personRepository.findPersonProjectedBy(); + + assertThat(collection, hasSize(3)); + + for (PersonProjection personProjection : collection) { + assertThat(personProjection.getLastname(), is(equalTo("White"))); + } + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByNumberOfChildren() throws Exception { + + assumeThat(SpringVersion.getVersion(), startsWith("4.3")); + + template.execute("CREATE INDEX IF NOT EXISTS person_number_of_children ON person (numberofchildren);"); + // Give Cassandra some time to build the index + Thread.sleep(500); + + Person result = personRepository.findByNumberOfChildren(NumberOfChildren.TWO); + + assertThat(result, is(walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldFindByLocalDate() throws InterruptedException { + + template.execute("CREATE INDEX IF NOT EXISTS person_created_date ON person (createddate);"); + // Give Cassandra some time to build the index + Thread.sleep(500); + + walter.setCreatedDate(LocalDate.now()); + personRepository.save(walter); + + Person result = personRepository.findByCreatedDate(walter.getCreatedDate()); + + assertThat(result, is(walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldUseQueryOverride() { + + Person otherWalter = new Person("Walter", "Black"); + personRepository.save(otherWalter); + + List result = personRepository.findByFirstname("Walter"); + + assertThat(result, hasSize(1)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldUseStartsWithQuery() throws InterruptedException { + + Version version = CassandraVersion.get(template.getSession()); + assumeTrue(version.isGreaterThanOrEqualTo(Version.parse("3.4"))); + + template.execute( + "CREATE CUSTOM INDEX IF NOT EXISTS fn_starts_with ON person (nickname) USING 'org.apache.cassandra.index.sasi.SASIIndex';"); + // Give Cassandra some time to build the index + Thread.sleep(500); + + walter.setNickname("Heisenberg"); + personRepository.save(walter); + + assertThat(personRepository.findByNicknameStartsWith("Heis"), is(walter)); + } + + /** + * @see DATACASS-7 + */ + @Test + public void shouldUseContainsQuery() throws InterruptedException { + + Version version = CassandraVersion.get(template.getSession()); + assumeTrue(version.isGreaterThanOrEqualTo(Version.parse("3.4"))); + + template.execute( + "CREATE CUSTOM INDEX IF NOT EXISTS fn_contains ON person (nickname) USING 'org.apache.cassandra.index.sasi.SASIIndex'\n" + + "WITH OPTIONS = { 'mode': 'CONTAINS' };"); + // Give Cassandra some time to build the index + Thread.sleep(500); + + walter.setNickname("Heisenberg"); + personRepository.save(walter); + + assertThat(personRepository.findByNicknameContains("eisenber"), is(walter)); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java index 0503034eb..927f1ae2b 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/AbstractSpringDataEmbeddedCassandraIntegrationTest.java @@ -39,6 +39,11 @@ public abstract class AbstractSpringDataEmbeddedCassandraIntegrationTest */ public void deleteAllEntities() { for (CassandraPersistentEntity entity : template.getConverter().getMappingContext().getPersistentEntities()) { + + if(entity.getType().isInterface()){ + continue; + } + template.truncate(entity.getTableName()); } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/CassandraVersion.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/CassandraVersion.java new file mode 100644 index 000000000..1b9d73547 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/support/CassandraVersion.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.test.integration.support; + +import lombok.experimental.UtilityClass; + +import org.springframework.data.util.Version; +import org.springframework.util.Assert; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; + +/** + * Utility to retrieve the Cassandra release version. + * + * @author Mark Paluch + */ +@UtilityClass +public class CassandraVersion { + + /** + * Retrieve the Cassandra release version. + * + * @param session must not be {@literal null}. + * @return the release {@link Version}. + */ + public static Version get(Session session) { + + Assert.notNull(session, "Session must not be null"); + + ResultSet resultSet = session.execute("SELECT release_version FROM system.local;"); + Row row = resultSet.one(); + return Version.parse(row.getString(0)); + } +} diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml index ec5fbec6b..3a78b36da 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/simple/UserRepositoryXmlConfigIntegrationTests-context.xml @@ -1,34 +1,15 @@ - + entity-base-packages="org.springframework.data.cassandra.test.integration.repository.simple">