diff --git a/spring-cql/src/main/java/org/springframework/cassandra/config/CassandraCqlClusterFactoryBean.java b/spring-cql/src/main/java/org/springframework/cassandra/config/CassandraCqlClusterFactoryBean.java index 57f5ce1ba..fd5b0bdc5 100644 --- a/spring-cql/src/main/java/org/springframework/cassandra/config/CassandraCqlClusterFactoryBean.java +++ b/spring-cql/src/main/java/org/springframework/cassandra/config/CassandraCqlClusterFactoryBean.java @@ -234,10 +234,6 @@ public class CassandraCqlClusterFactoryBean implements FactoryBean, Ini (CreateKeyspaceSpecification) spec).toCql() : new DropKeyspaceCqlGenerator( (DropKeyspaceSpecification) spec).toCql(); - if (log.isDebugEnabled()) { - log.debug("executing CQL [{}]", cql); - } - template.execute(cql); } } diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/CqlTemplate.java b/spring-cql/src/main/java/org/springframework/cassandra/core/CqlTemplate.java index 2b7492eda..6b15ce76b 100644 --- a/spring-cql/src/main/java/org/springframework/cassandra/core/CqlTemplate.java +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/CqlTemplate.java @@ -27,6 +27,8 @@ import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.cql.generator.AlterKeyspaceCqlGenerator; import org.springframework.cassandra.core.cql.generator.AlterTableCqlGenerator; @@ -89,6 +91,8 @@ import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; */ public class CqlTemplate extends CassandraAccessor implements CqlOperations { + protected static final Logger log = LoggerFactory.getLogger(CqlTemplate.class); + /** * Add common {@link Query} options for all types of queries. * @@ -455,6 +459,11 @@ public class CqlTemplate extends CassandraAccessor implements CqlOperations { @Override public ResultSet doInSession(Session s) throws DataAccessException { + + if (log.isDebugEnabled()) { + log.debug("executing [{}]", q.toString()); + } + return s.execute(q); } }); @@ -466,6 +475,10 @@ public class CqlTemplate extends CassandraAccessor implements CqlOperations { @Override public ResultSetFuture doInSession(Session s) throws DataAccessException { + + if (log.isDebugEnabled()) { + log.debug("asynchronously executing [{}]", q.toString()); + } return s.executeAsync(q); } }); diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/AbstractResultSetConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/AbstractResultSetConverter.java new file mode 100644 index 000000000..8193a58df --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/AbstractResultSetConverter.java @@ -0,0 +1,93 @@ +package org.springframework.cassandra.core.converter; + +import java.util.List; +import java.util.Map; + +import org.springframework.core.convert.converter.Converter; + +import com.datastax.driver.core.ResultSet; + +/** + * Convenient converter that can be used to convert a single-row-single-column, single-row-multi-column, or multi-row + * {@link ResultSet} into the a value of a given type. The majority of the expected usage is to convert a + * single-row-single-column result set into the target type. + *

+ * The algorithm is: + *

+ *

+ * If the converter throws due to the inability to convert a given {@link ResultSet}, it will throw an + * {@link IllegalArgumentException}. + * + * @author Matthew T. Adams + * + * @param + */ +public abstract class AbstractResultSetConverter implements Converter { + + /** + * Converts the given value to this converter's type or throws {@link IllegalArgumentException}. + */ + protected abstract T doConvertSingleValue(Object object); + + protected abstract Class getType(); + + protected ResultSetToListConverter converter = new ResultSetToListConverter(); + + protected T getNullResultSetValue() { + return null; + } + + protected T getExhaustedResultSetValue() { + return null; + } + + @Override + public T convert(ResultSet source) { + + if (source == null) { + return getNullResultSetValue(); + } + + if (source.isExhausted()) { + return getExhaustedResultSetValue(); + } + + List> list = converter.convert(source); + + if (list.size() == 1) { + + Map map = list.get(0); + return map.size() == 1 ? doConvertSingleValue(map.get(map.keySet().iterator().next())) : doConvertSingleRow(map); + } + + return doConvertResultSet(list); + } + + /** + * Converts the given result set (as a {@link List}<{@link Map}<String,Object>>) to this converter's type + * or throws {@link IllegalArgumentException}. This default implementation simply throws. + */ + protected T doConvertResultSet(List> resultSet) { + doThrow("result set"); + return null; + } + + /** + * Converts the given row (as a {@link Map}<String,Object>) to this converter's type or throws + * {@link IllegalArgumentException}. This default implementation simply throws. + */ + protected T doConvertSingleRow(Map row) { + doThrow("row"); + return null; + } + + protected void doThrow(String string) { + throw new IllegalArgumentException(String.format("can't convert %s to desired type [%s]", string, getType() + .getName())); + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/AbstractResultSetToBasicFixedTypeConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/AbstractResultSetToBasicFixedTypeConverter.java new file mode 100644 index 000000000..29c6eb2b6 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/AbstractResultSetToBasicFixedTypeConverter.java @@ -0,0 +1,16 @@ +package org.springframework.cassandra.core.converter; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; + +/** + * Thin wrapper that allows subclasses to delegate conversion of the given value to a {@link DefaultConversionService}. + * + * @author Matthew T. Adams + * + * @param + */ +public abstract class AbstractResultSetToBasicFixedTypeConverter extends AbstractResultSetConverter { + + protected final static ConversionService CONVERTER = new DefaultConversionService(); +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToArrayConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToArrayConverter.java new file mode 100644 index 000000000..ef8ac540c --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToArrayConverter.java @@ -0,0 +1,46 @@ +package org.springframework.cassandra.core.converter; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.Assert; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; + +public class ResultSetToArrayConverter implements Converter { + + protected Converter rowConverter; + + public ResultSetToArrayConverter(Converter rowConverter) { + setRowConverter(rowConverter); + } + + public Converter getRowConverter() { + return rowConverter; + } + + public void setRowConverter(Converter rowConverter) { + + Assert.notNull(rowConverter); + this.rowConverter = rowConverter; + } + + @Override + public Object[] convert(ResultSet resultSet) { + + if (resultSet == null) { + return null; + } + + List list = new ArrayList(); + Iterator i = resultSet.iterator(); + while (i.hasNext()) { + list.add(rowConverter.convert(i.next())); + } + + return list.toArray(); + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToBigDecimalConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToBigDecimalConverter.java new file mode 100644 index 000000000..be32ecb2a --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToBigDecimalConverter.java @@ -0,0 +1,16 @@ +package org.springframework.cassandra.core.converter; + +import java.math.BigDecimal; + +public class ResultSetToBigDecimalConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected BigDecimal doConvertSingleValue(Object object) { + return CONVERTER.convert(object, BigDecimal.class); + } + + @Override + protected Class getType() { + return BigDecimal.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToBigIntegerConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToBigIntegerConverter.java new file mode 100644 index 000000000..acfee7288 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToBigIntegerConverter.java @@ -0,0 +1,16 @@ +package org.springframework.cassandra.core.converter; + +import java.math.BigInteger; + +public class ResultSetToBigIntegerConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected BigInteger doConvertSingleValue(Object object) { + return CONVERTER.convert(object, BigInteger.class); + } + + @Override + protected Class getType() { + return BigInteger.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToBooleanConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToBooleanConverter.java new file mode 100644 index 000000000..bac02da22 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToBooleanConverter.java @@ -0,0 +1,14 @@ +package org.springframework.cassandra.core.converter; + +public class ResultSetToBooleanConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected Boolean doConvertSingleValue(Object object) { + return CONVERTER.convert(object, Boolean.class); + } + + @Override + protected Class getType() { + return Boolean.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToByteBufferConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToByteBufferConverter.java new file mode 100644 index 000000000..859dd3689 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToByteBufferConverter.java @@ -0,0 +1,21 @@ +package org.springframework.cassandra.core.converter; + +import java.nio.ByteBuffer; + +public class ResultSetToByteBufferConverter extends AbstractResultSetConverter { + + @Override + protected ByteBuffer doConvertSingleValue(Object object) { + + if (!(object instanceof ByteBuffer)) { + doThrow("value"); + } + + return (ByteBuffer) object; + } + + @Override + protected Class getType() { + return ByteBuffer.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToDateConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToDateConverter.java new file mode 100644 index 000000000..8737754ae --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToDateConverter.java @@ -0,0 +1,16 @@ +package org.springframework.cassandra.core.converter; + +import java.util.Date; + +public class ResultSetToDateConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected Date doConvertSingleValue(Object object) { + return CONVERTER.convert(object, Date.class); + } + + @Override + protected Class getType() { + return Date.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToDoubleConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToDoubleConverter.java new file mode 100644 index 000000000..dda7fae99 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToDoubleConverter.java @@ -0,0 +1,14 @@ +package org.springframework.cassandra.core.converter; + +public class ResultSetToDoubleConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected Double doConvertSingleValue(Object object) { + return CONVERTER.convert(object, Double.class); + } + + @Override + protected Class getType() { + return Double.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToFloatConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToFloatConverter.java new file mode 100644 index 000000000..a457bb685 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToFloatConverter.java @@ -0,0 +1,14 @@ +package org.springframework.cassandra.core.converter; + +public class ResultSetToFloatConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected Float doConvertSingleValue(Object object) { + return CONVERTER.convert(object, Float.class); + } + + @Override + protected Class getType() { + return Float.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToInetAddressConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToInetAddressConverter.java new file mode 100644 index 000000000..8a339273c --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToInetAddressConverter.java @@ -0,0 +1,16 @@ +package org.springframework.cassandra.core.converter; + +import java.net.InetAddress; + +public class ResultSetToInetAddressConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected InetAddress doConvertSingleValue(Object object) { + return CONVERTER.convert(object, InetAddress.class); + } + + @Override + protected Class getType() { + return InetAddress.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToIntegerConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToIntegerConverter.java new file mode 100644 index 000000000..5d9bbe273 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToIntegerConverter.java @@ -0,0 +1,14 @@ +package org.springframework.cassandra.core.converter; + +public class ResultSetToIntegerConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected Integer doConvertSingleValue(Object object) { + return CONVERTER.convert(object, Integer.class); + } + + @Override + protected Class getType() { + return Integer.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToListConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToListConverter.java new file mode 100644 index 000000000..aef37815d --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToListConverter.java @@ -0,0 +1,50 @@ +package org.springframework.cassandra.core.converter; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.Assert; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; + +public class ResultSetToListConverter implements Converter>> { + + protected Converter> rowConverter = new RowToMapConverter(); + + public ResultSetToListConverter() { + } + + public ResultSetToListConverter(Converter> rowConverter) { + setRowConverter(rowConverter); + } + + public Converter> getRowConverter() { + return rowConverter; + } + + public void setRowConverter(Converter> rowConverter) { + + Assert.notNull(rowConverter); + this.rowConverter = rowConverter; + } + + @Override + public List> convert(ResultSet resultSet) { + + if (resultSet == null) { + return null; + } + + List> list = new ArrayList>(); + Iterator i = resultSet.iterator(); + while (i.hasNext()) { + list.add(rowConverter.convert(i.next())); + } + + return list; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToListOfStringConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToListOfStringConverter.java new file mode 100644 index 000000000..09b3e19bb --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToListOfStringConverter.java @@ -0,0 +1,48 @@ +package org.springframework.cassandra.core.converter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.util.StringUtils; + +public class ResultSetToListOfStringConverter extends AbstractResultSetConverter> { + + @Override + protected List doConvertSingleValue(Object object) { + + List list = new ArrayList(); + + list.add(object == null ? null : object.toString()); + return list; + } + + @Override + protected List doConvertSingleRow(Map row) { + + List list = new ArrayList(row.size()); + + for (Object value : row.values()) { + list.add(value == null ? null : value.toString()); + } + + return list; + } + + @Override + protected List doConvertResultSet(List> resultSet) { + + List list = new ArrayList(resultSet.size()); + + for (Map row : resultSet) { + list.add(StringUtils.arrayToCommaDelimitedString(doConvertSingleRow(row).toArray())); + } + + return list; + } + + @Override + protected Class getType() { + return List.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToLongConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToLongConverter.java new file mode 100644 index 000000000..43ceba9d8 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToLongConverter.java @@ -0,0 +1,14 @@ +package org.springframework.cassandra.core.converter; + +public class ResultSetToLongConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected Long doConvertSingleValue(Object object) { + return CONVERTER.convert(object, Long.class); + } + + @Override + protected Class getType() { + return Long.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToStringConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToStringConverter.java new file mode 100644 index 000000000..c4e0e4fc7 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToStringConverter.java @@ -0,0 +1,61 @@ +package org.springframework.cassandra.core.converter; + +import java.util.List; +import java.util.Map; + +public class ResultSetToStringConverter extends AbstractResultSetConverter { + + @Override + protected String doConvertSingleValue(Object object) { + return object == null ? null : object.toString(); + } + + @Override + protected String doConvertSingleRow(Map row) { + + StringBuilder s = new StringBuilder(); + + boolean firstEntry = true; + + for (Map.Entry entry : row.entrySet()) { + + if (firstEntry) { + firstEntry = false; + } else { + s.append(", "); + } + + s.append("\"").append(entry.getKey().replaceAll("\"", "\\\"")).append("\""); + s.append(" : "); + s.append("\"").append(entry.getValue().toString().replaceAll("\"", "\\\"")).append("\""); + } + + return s.toString(); + } + + @Override + protected String doConvertResultSet(List> resultSet) { + + boolean firstElement = true; + StringBuilder s = new StringBuilder("{ "); + + for (Map map : resultSet) { + + if (firstElement) { + firstElement = false; + } else { + s.append(", "); + } + + s.append(doConvertSingleRow(map)); + } + s.append(" }"); + + return s.toString(); + } + + @Override + protected Class getType() { + return String.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToUuidConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToUuidConverter.java new file mode 100644 index 000000000..4d7889413 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/ResultSetToUuidConverter.java @@ -0,0 +1,16 @@ +package org.springframework.cassandra.core.converter; + +import java.util.UUID; + +public class ResultSetToUuidConverter extends AbstractResultSetToBasicFixedTypeConverter { + + @Override + protected UUID doConvertSingleValue(Object object) { + return CONVERTER.convert(object, UUID.class); + } + + @Override + protected Class getType() { + return UUID.class; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToArrayConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToArrayConverter.java new file mode 100644 index 000000000..aa6618e36 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToArrayConverter.java @@ -0,0 +1,19 @@ +package org.springframework.cassandra.core.converter; + +import java.util.List; + +import org.springframework.core.convert.converter.Converter; + +import com.datastax.driver.core.Row; + +public class RowToArrayConverter implements Converter { + + protected RowToListConverter delegate = new RowToListConverter(); + + @Override + public Object[] convert(Row row) { + + List list = delegate.convert(row); + return list == null ? null : list.toArray(); + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToListConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToListConverter.java new file mode 100644 index 000000000..78911033e --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToListConverter.java @@ -0,0 +1,31 @@ +package org.springframework.cassandra.core.converter; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.convert.converter.Converter; + +import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.ColumnDefinitions.Definition; + +public class RowToListConverter implements Converter> { + + @Override + public List convert(Row row) { + + if (row == null) { + return null; + } + + ColumnDefinitions cols = row.getColumnDefinitions(); + List list = new ArrayList(cols.size()); + + for (Definition def : cols.asList()) { + String name = def.getName(); + list.add(row.isNull(name) ? null : def.getType().deserialize(row.getBytesUnsafe(name))); + } + + return list; + } +} diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToMapConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToMapConverter.java new file mode 100644 index 000000000..e7ec242c3 --- /dev/null +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToMapConverter.java @@ -0,0 +1,32 @@ +package org.springframework.cassandra.core.converter; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.convert.converter.Converter; + +import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.ColumnDefinitions.Definition; +import com.datastax.driver.core.Row; + +public class RowToMapConverter implements Converter> { + + @Override + public Map convert(Row row) { + + if (row == null) { + return null; + } + + ColumnDefinitions cols = row.getColumnDefinitions(); + Map map = new HashMap(cols.size()); + + for (Definition def : cols.asList()) { + + String name = def.getName(); + map.put(name, row.isNull(name) ? null : def.getType().deserialize(row.getBytesUnsafe(name))); + } + + return map; + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/BasicCassandraRowValueProvider.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/BasicCassandraRowValueProvider.java index 5f76d6598..0cef42732 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/BasicCassandraRowValueProvider.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/BasicCassandraRowValueProvider.java @@ -15,8 +15,6 @@ */ package org.springframework.data.cassandra.convert; -import java.util.List; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; @@ -25,7 +23,6 @@ import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.util.Assert; -import com.datastax.driver.core.DataType; import com.datastax.driver.core.Row; /** @@ -37,9 +34,7 @@ import com.datastax.driver.core.Row; */ public class BasicCassandraRowValueProvider implements CassandraRowValueProvider { - private static Logger log = LoggerFactory.getLogger(BasicCassandraRowValueProvider.class); - - private final Row source; + private final ColumnReader reader; private final SpELExpressionEvaluator evaluator; /** @@ -53,88 +48,24 @@ public class BasicCassandraRowValueProvider implements CassandraRowValueProvider Assert.notNull(source); Assert.notNull(evaluator); - this.source = source; + this.reader = new ColumnReader(source); this.evaluator = evaluator; } @Override @SuppressWarnings("unchecked") - public T getPropertyValue(CassandraPersistentProperty property) { + public Object getPropertyValue(CassandraPersistentProperty property) { String expression = property.getSpelExpression(); if (expression != null) { return evaluator.evaluate(expression); } - String columnName = property.getColumnName().toCql(); - if (source.isNull(columnName)) { - return null; - } - - DataType columnType = source.getColumnDefinitions().getType(columnName); - - /* - * Handle the types of collections that are available - */ - if (columnType.isCollection()) { - - List collectionTypes = columnType.getTypeArguments(); - if (collectionTypes.size() == 2) { - return (T) source - .getMap(columnName, collectionTypes.get(0).asJavaClass(), collectionTypes.get(1).asJavaClass()); - } - - if (columnType.equals(DataType.list(collectionTypes.get(0)))) { - return (T) source.getList(columnName, collectionTypes.get(0).asJavaClass()); - } - - if (columnType.equals(DataType.set(collectionTypes.get(0)))) { - return (T) source.getSet(columnName, collectionTypes.get(0).asJavaClass()); - } - - throw new IllegalStateException("Unknown Collection type encountered. Valid collections are Set, List and Map."); - } - - if (columnType.equals(DataType.text()) || columnType.equals(DataType.ascii()) - || columnType.equals(DataType.varchar())) { - return (T) source.getString(columnName); - } - if (columnType.equals(DataType.cint()) || columnType.equals(DataType.varint())) { - return (T) new Integer(source.getInt(columnName)); - } - if (columnType.equals(DataType.cdouble())) { - return (T) new Double(source.getDouble(columnName)); - } - if (columnType.equals(DataType.bigint()) || columnType.equals(DataType.counter())) { - return (T) new Long(source.getLong(columnName)); - } - if (columnType.equals(DataType.cfloat())) { - return (T) new Float(source.getFloat(columnName)); - } - if (columnType.equals(DataType.decimal())) { - return (T) source.getDecimal(columnName); - } - if (columnType.equals(DataType.cboolean())) { - return (T) new Boolean(source.getBool(columnName)); - } - if (columnType.equals(DataType.timestamp())) { - return (T) source.getDate(columnName); - } - if (columnType.equals(DataType.blob())) { - return (T) source.getBytes(columnName); - } - if (columnType.equals(DataType.inet())) { - return (T) source.getInet(columnName); - } - if (columnType.equals(DataType.uuid()) || columnType.equals(DataType.timeuuid())) { - return (T) source.getUUID(columnName); - } - - return (T) source.getBytes(columnName); + return reader.get(property.getColumnName()); } @Override public Row getRow() { - return source; + return reader.getRow(); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ColumnReader.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ColumnReader.java new file mode 100644 index 000000000..2ea6dd178 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ColumnReader.java @@ -0,0 +1,142 @@ +package org.springframework.data.cassandra.convert; + +import java.util.List; + +import org.springframework.cassandra.core.cql.CqlIdentifier; + +import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.Row; + +/** + * Helpful class to read a column's value from a row, with possible type conversion. + * + * @author Matthew T. Adams + */ +public class ColumnReader { + + protected Row row; + protected ColumnDefinitions columns; + + public ColumnReader(Row row) { + this.row = row; + this.columns = row.getColumnDefinitions(); + } + + /** + * Returns the row's column value. + */ + public Object get(CqlIdentifier name) { + return get(name.toCql()); + } + + /** + * Returns the row's column value. + */ + public Object get(String name) { + return get(columns.getIndexOf(name)); + } + + public Object get(int i) { + + if (row.isNull(i)) { + return null; + } + + DataType type = columns.getType(i); + + if (type.isCollection()) { + + List collectionTypes = type.getTypeArguments(); + if (collectionTypes.size() == 2) { + return row.getMap(i, collectionTypes.get(0).asJavaClass(), collectionTypes.get(1).asJavaClass()); + } + + if (type.equals(DataType.list(collectionTypes.get(0)))) { + return row.getList(i, collectionTypes.get(0).asJavaClass()); + } + + if (type.equals(DataType.set(collectionTypes.get(0)))) { + return row.getSet(i, collectionTypes.get(0).asJavaClass()); + } + + throw new IllegalStateException("Unknown Collection type encountered. Valid collections are Set, List and Map."); + } + + if (type.equals(DataType.text()) || type.equals(DataType.ascii()) || type.equals(DataType.varchar())) { + return row.getString(i); + } + if (type.equals(DataType.cint()) || type.equals(DataType.varint())) { + return new Integer(row.getInt(i)); + } + if (type.equals(DataType.cdouble())) { + return new Double(row.getDouble(i)); + } + if (type.equals(DataType.bigint()) || type.equals(DataType.counter())) { + return new Long(row.getLong(i)); + } + if (type.equals(DataType.cfloat())) { + return new Float(row.getFloat(i)); + } + if (type.equals(DataType.decimal())) { + return row.getDecimal(i); + } + if (type.equals(DataType.cboolean())) { + return new Boolean(row.getBool(i)); + } + if (type.equals(DataType.timestamp())) { + return row.getDate(i); + } + if (type.equals(DataType.blob())) { + return row.getBytes(i); + } + if (type.equals(DataType.inet())) { + return row.getInet(i); + } + if (type.equals(DataType.uuid()) || type.equals(DataType.timeuuid())) { + return row.getUUID(i); + } + + return row.getBytesUnsafe(i); + } + + public Row getRow() { + return row; + } + + /** + * Returns the row's column value as an instance of the given type. + * + * @throws ClassCastException if the value cannot be converted to the requested type. + */ + public T get(CqlIdentifier name, Class requestedType) { + return get(columns.getIndexOf(name.toCql()), requestedType); + } + + /** + * Returns the row's column value as an instance of the given type. + * + * @throws ClassCastException if the value cannot be converted to the requested type. + */ + public T get(String name, Class requestedType) { + return get(columns.getIndexOf(name), requestedType); + } + + /** + * Returns the row's column value as an instance of the given type. + * + * @throws ClassCastException if the value cannot be converted to the requested type. + */ + @SuppressWarnings("unchecked") + public T get(int i, Class requestedType) { + + Object o = get(i); + + if (o == null) { + return null; + } + + return (T) o; + } + +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java index b80158f5d..52ef09607 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/MappingCassandraConverter.java @@ -15,7 +15,7 @@ */ package org.springframework.data.cassandra.convert; -import static org.springframework.data.cassandra.repository.support.BasicMapId.id; +import static org.springframework.data.cassandra.repository.support.BasicMapId.*; import java.io.Serializable; import java.util.Map; @@ -56,6 +56,7 @@ import com.datastax.driver.core.querybuilder.Update; * * @author Alex Shvid * @author Matthew T. Adams + * @author Oliver Gierke */ public class MappingCassandraConverter extends AbstractCassandraConverter implements CassandraConverter, ApplicationContextAware, BeanClassLoaderAware { @@ -65,7 +66,6 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem protected final CassandraMappingContext mappingContext; protected ApplicationContext applicationContext; protected SpELContext spELContext; - protected boolean useFieldAccessOnly = true; protected ClassLoader beanClassLoader; @@ -124,7 +124,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); S instance = instantiator.createInstance(entity, parameterProvider); - BeanWrapper, S> wrapper = BeanWrapper.create(instance, conversionService); + BeanWrapper wrapper = BeanWrapper.create(instance, conversionService); readPropertiesFromRow(entity, rowValueProvider, wrapper); @@ -132,7 +132,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem } protected void readPropertiesFromRow(final CassandraPersistentEntity entity, - final BasicCassandraRowValueProvider row, final BeanWrapper wrapper) { + final BasicCassandraRowValueProvider row, final BeanWrapper wrapper) { entity.doWithProperties(new PropertyHandler() { @@ -145,7 +145,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem } protected void readPropertyFromRow(final CassandraPersistentEntity entity, final CassandraPersistentProperty prop, - final BasicCassandraRowValueProvider row, final BeanWrapper wrapper) { + final BasicCassandraRowValueProvider row, final BeanWrapper wrapper) { if (entity.isConstructorArgument(prop)) { // skip 'cause prop was set in ctor return; @@ -161,14 +161,13 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem } // wrap the key - @SuppressWarnings("rawtypes") - BeanWrapper keyWrapper = BeanWrapper.create(key, conversionService); + BeanWrapper keyWrapper = BeanWrapper.create(key, conversionService); // now recurse on using the key this time readPropertiesFromRow(prop.getCompositePrimaryKeyEntity(), row, keyWrapper); // now that the key's properties have been populated, set the key property on the entity - wrapper.setProperty(keyProperty, keyWrapper.getBean(), useFieldAccessOnly); + wrapper.setProperty(keyProperty, keyWrapper.getBean()); return; } @@ -177,7 +176,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem } Object obj = row.getPropertyValue(prop); - wrapper.setProperty(prop, obj, useFieldAccessOnly); + wrapper.setProperty(prop, obj); } protected Object instantiatePrimaryKey(CassandraPersistentEntity entity, CassandraPersistentProperty keyProperty, @@ -189,14 +188,6 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem propertyProvider, null)); } - public boolean getUseFieldAccessOnly() { - return useFieldAccessOnly; - } - - public void setUseFieldAccessOnly(boolean useFieldAccessOnly) { - this.useFieldAccessOnly = useFieldAccessOnly; - } - @Override public R read(Class type, Object row) { if (row instanceof Row) { @@ -231,23 +222,21 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem } protected void writeInsertFromObject(final Object object, final Insert insert, CassandraPersistentEntity entity) { - writeInsertFromWrapper(BeanWrapper., Object> create(object, conversionService), - insert, entity); + writeInsertFromWrapper(BeanWrapper.create(object, conversionService), insert, entity); } - protected void writeInsertFromWrapper(final BeanWrapper, Object> wrapper, - final Insert insert, CassandraPersistentEntity entity) { + protected void writeInsertFromWrapper(final BeanWrapper wrapper, final Insert insert, + CassandraPersistentEntity entity) { entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(CassandraPersistentProperty prop) { - Object value = wrapper.getProperty(prop, prop.getType(), useFieldAccessOnly); + Object value = wrapper.getProperty(prop, prop.getType()); if (prop.isCompositePrimaryKey()) { - writeInsertFromWrapper( - BeanWrapper., Object> create(value, conversionService), insert, + writeInsertFromWrapper(BeanWrapper.create(value, conversionService), insert, prop.getCompositePrimaryKeyEntity()); return; } @@ -260,23 +249,21 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem } protected void writeUpdateFromObject(final Object object, final Update update, CassandraPersistentEntity entity) { - writeUpdateFromWrapper(BeanWrapper., Object> create(object, conversionService), - update, entity); + writeUpdateFromWrapper(BeanWrapper.create(object, conversionService), update, entity); } - protected void writeUpdateFromWrapper(final BeanWrapper, Object> wrapper, - final Update update, final CassandraPersistentEntity entity) { + protected void writeUpdateFromWrapper(final BeanWrapper wrapper, final Update update, + final CassandraPersistentEntity entity) { entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(CassandraPersistentProperty prop) { - Object value = wrapper.getProperty(prop, prop.getType(), useFieldAccessOnly); + Object value = wrapper.getProperty(prop, prop.getType()); if (prop.isCompositePrimaryKey()) { - writeUpdateFromWrapper( - BeanWrapper., Object> create(value, conversionService), update, + writeUpdateFromWrapper(BeanWrapper.create(value, conversionService), update, prop.getCompositePrimaryKeyEntity()); return; } @@ -293,12 +280,11 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem } protected void writeDeleteWhereFromObject(final Object object, final Where where, CassandraPersistentEntity entity) { - writeDeleteWhereFromWrapper( - BeanWrapper., Object> create(object, conversionService), where, entity); + writeDeleteWhereFromWrapper(BeanWrapper.create(object, conversionService), where, entity); } - protected void writeDeleteWhereFromWrapper(final BeanWrapper, Object> wrapper, - final Where where, CassandraPersistentEntity entity) { + protected void writeDeleteWhereFromWrapper(final BeanWrapper wrapper, final Where where, + CassandraPersistentEntity entity) { Object id = getId(wrapper, entity); if (id == null) { @@ -319,8 +305,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem if (idProperty != null) { if (idProperty.isCompositePrimaryKey()) { - writeDeleteWhereFromWrapper( - BeanWrapper., Object> create(id, conversionService), where, + writeDeleteWhereFromWrapper(BeanWrapper.create(id, conversionService), where, idProperty.getCompositePrimaryKeyEntity()); return; } @@ -335,8 +320,8 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem Assert.notNull(object); - final BeanWrapper wrapper = (object instanceof BeanWrapper) ? (BeanWrapper) object : BeanWrapper - .create(object, conversionService); + final BeanWrapper wrapper = object instanceof BeanWrapper ? (BeanWrapper) object : BeanWrapper.create(object, + conversionService); object = wrapper == null ? object : wrapper.getBean(); if (!entity.getType().isAssignableFrom(object.getClass())) { @@ -351,7 +336,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem CassandraPersistentProperty idProperty = entity.getIdProperty(); if (idProperty != null) { - return wrapper.getProperty(entity.getIdProperty(), idProperty.getType(), useFieldAccessOnly); + return wrapper.getProperty(entity.getIdProperty(), idProperty.getType()); } // if the class doesn't have an id property, then it's using MapId @@ -361,7 +346,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter implem @Override public void doWithPersistentProperty(CassandraPersistentProperty p) { if (p.isPrimaryKeyColumn()) { - id.with(p.getName(), (Serializable) wrapper.getProperty(p, p.getType(), useFieldAccessOnly)); + id.with(p.getName(), (Serializable) wrapper.getProperty(p, p.getType())); } } }); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java index f772b21c7..3b2dd6827 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraAdminTemplate.java @@ -62,6 +62,8 @@ public class CassandraAdminTemplate extends CassandraTemplate implements Cassand String cql = new CreateTableCqlGenerator(getCassandraMappingContext().getCreateTableSpecificationFor(entity)) .toCql(); + log.debug(cql); + s.execute(cql); return null; } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java index 1a8f6b390..372eac274 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java @@ -55,20 +55,18 @@ import com.datastax.driver.core.querybuilder.Update; * @author Alex Shvid * @author David Webb * @author Matthew T. Adams - * + * @author Oliver Gierke * @see CqlTemplate */ public class CassandraTemplate extends CqlTemplate implements CassandraOperations { protected CassandraConverter cassandraConverter; protected CassandraMappingContext mappingContext; - protected boolean useFieldAccessOnly = false; /** * Default Constructor for wiring in the required components later */ - public CassandraTemplate() { - } + public CassandraTemplate() {} /** * Constructor if only session and converter are known at time of Template Creation @@ -98,19 +96,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation return mappingContext; } - public boolean getUseFieldAccessOnly() { - return useFieldAccessOnly; - } - - /** - * Whether only fields should be used when accessing a persistent entity's data. - * - * @param useFieldAccessOnly - */ - public void setUseFieldAccessOnly(boolean useFieldAccessOnly) { - this.useFieldAccessOnly = useFieldAccessOnly; - } - @Override public void afterPropertiesSet() { super.afterPropertiesSet(); @@ -313,8 +298,7 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation CassandraPersistentEntity idEntity = idProperty.getCompositePrimaryKeyEntity(); - final BeanWrapper, Object> idWrapper = BeanWrapper - ., Object> create(id, cassandraConverter.getConversionService()); + final BeanWrapper idWrapper = BeanWrapper.create(id, cassandraConverter.getConversionService()); idEntity.doWithProperties(new PropertyHandler() { @@ -322,7 +306,7 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation public void doWithPersistentProperty(CassandraPersistentProperty p) { clauseCallback.doWithClause(QueryBuilder.eq(p.getColumnName().toCql(), - idWrapper.getProperty(p, p.getActualType(), useFieldAccessOnly))); + idWrapper.getProperty(p, p.getActualType()))); } }); @@ -595,7 +579,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation * @param objectToSave * @param entity * @param optionsByName - * * @return The Query object to run with session.execute(); */ public static Insert createInsertQuery(String tableName, Object objectToSave, QueryOptions options, @@ -623,7 +606,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation * @param objectToSave * @param entity * @param optionsByName - * * @return The Query object to run with session.execute(); */ public static Update toUpdateQuery(String tableName, Object objectToSave, QueryOptions options, @@ -651,7 +633,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation * @param objectsToSave * @param entity * @param optionsByName - * * @return The Query object to run with session.execute(); */ public static Batch toUpdateBatchQuery(String tableName, List objectsToSave, QueryOptions options, @@ -684,7 +665,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation * @param entities * @param entity * @param optionsByName - * * @return The Query object to run with session.execute(); */ public static Batch createInsertBatchQuery(String tableName, List entities, QueryOptions options, @@ -731,7 +711,6 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation * @param entities * @param entity * @param optionsByName - * * @return */ public static Batch createDeleteBatchQuery(String tableName, List entities, QueryOptions options, 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 cf0867ada..6e84e1ec5 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,7 +42,7 @@ public class BasicCassandraPersistentEntityMetadataVerifier implements Cassandra @Override public void verify(CassandraPersistentEntity entity) throws MappingException { - VerifierMappingExceptions exceptions = new VerifierMappingExceptions( + VerifierMappingExceptions exceptions = new VerifierMappingExceptions(entity, "Mapping Exceptions from BasicCassandraPersistentEntityMetadataVerifier"); final List idProperties = new ArrayList(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraPersistentProperty.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraPersistentProperty.java index 2630c8f80..7a94d654e 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraPersistentProperty.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraPersistentProperty.java @@ -20,6 +20,7 @@ import java.util.List; import org.springframework.cassandra.core.Ordering; import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.util.TypeInformation; @@ -131,4 +132,14 @@ public interface CassandraPersistentProperty extends PersistentProperty columnNames); + + public enum PropertyToFieldNameConverter implements Converter { + + INSTANCE; + + @Override + public String convert(CassandraPersistentProperty source) { + return source.getColumnName().toCql(); + } + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/VerifierMappingExceptions.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/VerifierMappingExceptions.java index 84db4b4c0..695d701e4 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/VerifierMappingExceptions.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/VerifierMappingExceptions.java @@ -31,12 +31,14 @@ import org.springframework.data.mapping.model.MappingException; public class VerifierMappingExceptions extends MappingException { Collection exceptions = new LinkedList(); + private String className; /** * @param s */ - public VerifierMappingExceptions(String s) { + public VerifierMappingExceptions(CassandraPersistentEntity entity, String s) { super(s); + this.className = entity.getType().getName(); } /** @@ -79,7 +81,7 @@ public class VerifierMappingExceptions extends MappingException { @Override public String getMessage() { - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new StringBuilder(className).append(":\n"); for (MappingException e : exceptions) { builder.append(e.getMessage()).append("\n"); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/Query.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/Query.java index 2d22e91d9..45dcc093b 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/Query.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/Query.java @@ -21,22 +21,25 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.data.annotation.QueryAnnotation; + /** * Annotation to declare finder queries directly on repository methods. * * @author Alex Shvid + * @author Matthew T. Adams */ - @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented +@QueryAnnotation public @interface Query { /** - * Takes a Cassandra CQL3 string to define the actual query to be executed. + * A Cassandra CQL3 string to define the actual query to be executed. Placeholders {@code ?0}, {@code ?1}, etc are + * supported. * * @return */ String value() default ""; - } 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 new file mode 100644 index 000000000..8308c8447 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java @@ -0,0 +1,212 @@ +/* + * Copyright 2010-2014 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.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cassandra.core.converter.ResultSetToBigDecimalConverter; +import org.springframework.cassandra.core.converter.ResultSetToBigIntegerConverter; +import org.springframework.cassandra.core.converter.ResultSetToBooleanConverter; +import org.springframework.cassandra.core.converter.ResultSetToByteBufferConverter; +import org.springframework.cassandra.core.converter.ResultSetToDateConverter; +import org.springframework.cassandra.core.converter.ResultSetToDoubleConverter; +import org.springframework.cassandra.core.converter.ResultSetToFloatConverter; +import org.springframework.cassandra.core.converter.ResultSetToInetAddressConverter; +import org.springframework.cassandra.core.converter.ResultSetToIntegerConverter; +import org.springframework.cassandra.core.converter.ResultSetToListConverter; +import org.springframework.cassandra.core.converter.ResultSetToLongConverter; +import org.springframework.cassandra.core.converter.ResultSetToStringConverter; +import org.springframework.cassandra.core.converter.ResultSetToUuidConverter; +import org.springframework.cassandra.core.converter.RowToMapConverter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; + +/** + * Base class for {@link RepositoryQuery} implementations for Cassandra. + */ +public abstract class AbstractCassandraQuery implements RepositoryQuery { + + protected static final Converter[] DEFAULT_CONVERTERS = new Converter[] { new ResultSetToListConverter(), + new ResultSetToStringConverter(), new RowToMapConverter(), new ResultSetToBigDecimalConverter(), + new ResultSetToBigIntegerConverter(), new ResultSetToBooleanConverter(), new ResultSetToByteBufferConverter(), + new ResultSetToDateConverter(), new ResultSetToDoubleConverter(), new ResultSetToFloatConverter(), + new ResultSetToInetAddressConverter(), new ResultSetToIntegerConverter(), new ResultSetToLongConverter(), + new ResultSetToUuidConverter() }; + + protected static Logger log = LoggerFactory.getLogger(AbstractCassandraQuery.class); + + private ConversionService conversionService; + + Converter>> resultSetToListConverter = new ResultSetToListConverter(); + + private final CassandraQueryMethod method; + private final CassandraOperations template; + + protected RowToMapConverter rowToMapConverter = new RowToMapConverter(); + + /** + * Creates a new {@link AbstractCassandraQuery} from the given {@link CassandraQueryMethod} and + * {@link CassandraOperations}. + * + * @param method must not be {@literal null}. + * @param template must not be {@literal null}. + */ + public AbstractCassandraQuery(CassandraQueryMethod method, CassandraOperations operations) { + + Assert.notNull(operations); + Assert.notNull(method); + + this.method = method; + this.template = operations; + + this.conversionService = createDefaultConversionService(); + } + + protected ConfigurableConversionService createDefaultConversionService() { + + ConfigurableConversionService conversionService = new DefaultConversionService(); + + for (Converter converter : DEFAULT_CONVERTERS) { + conversionService.addConverter(converter); + } + + return conversionService; + } + + @Override + public CassandraQueryMethod getQueryMethod() { + return method; + } + + @Override + public Object execute(Object[] parameters) { + + CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(method, parameters); + String query = createQuery(accessor); + + ResultSet resultSet = template.query(query); + + // return raw result set if requested + if (method.isResultSetQuery()) { + return resultSet; + } + + Class declaredReturnType = method.getReturnType().getType(); + Class returnedUnwrappedObjectType = method.getReturnedObjectType(); + + if (method.isSingleEntityQuery()) { + return getSingleEntity(resultSet, returnedUnwrappedObjectType); + } + + Object retval = resultSet; + + if (method.isCollectionOfEntityQuery()) { + retval = getCollectionOfEntity(resultSet, declaredReturnType, returnedUnwrappedObjectType); + } + + // TODO: support Page & Slice queries + + // if we get this far, let the configured conversion service try to convert the result set + return conversionService.convert(retval, TypeDescriptor.forObject(retval), + TypeDescriptor.valueOf(declaredReturnType)); + } + + public Object getCollectionOfEntity(ResultSet resultSet, Class declaredReturnType, + Class returnedUnwrappedObjectType) { + + Collection results = null; + + if (ClassUtils.isAssignable(SortedSet.class, declaredReturnType)) { + results = new TreeSet(); + } else if (ClassUtils.isAssignable(Set.class, declaredReturnType)) { + results = new HashSet(); + } else { // List.class, Collection.class, or array + results = new ArrayList(); + } + + CassandraConverter converter = template.getConverter(); + for (Row row : resultSet) { + results.add(converter.read(returnedUnwrappedObjectType, row)); + } + + return results; + } + + public Object getSingleEntity(ResultSet resultSet, Class type) { + if (resultSet.isExhausted()) { + return null; + } + + Iterator iterator = resultSet.iterator(); + Object object = template.getConverter().read(type, iterator.next()); + + warnIfMoreResults(iterator); + + return object; + } + + protected void warnIfMoreResults(Iterator iterator) { + if (log.isWarnEnabled() && iterator.hasNext()) { + + int i = 0; + while (iterator.hasNext()) { + iterator.next(); + i++; + } + + log.warn("ignoring extra {} row{}", i, i == 1 ? "" : "s"); + } + } + + public ConversionService getConversionService() { + return conversionService; + } + + public void setConversionService(ConversionService conversionService) { + + Assert.notNull(conversionService); + this.conversionService = conversionService; + } + + /** + * Creates a string query using the given {@link ParameterAccessor} + * + * @param accessor must not be {@literal null}. + */ + protected abstract String createQuery(CassandraParameterAccessor accessor); +} 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 new file mode 100644 index 000000000..0261f7704 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java @@ -0,0 +1,6 @@ +package org.springframework.data.cassandra.repository.query; + +import org.springframework.data.repository.query.ParameterAccessor; + +public interface CassandraParameterAccessor extends ParameterAccessor { +} 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 new file mode 100644 index 000000000..aba240973 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java @@ -0,0 +1,37 @@ +package org.springframework.data.cassandra.repository.query; + +import java.lang.reflect.Method; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; + +public class CassandraParameters extends Parameters { + + public CassandraParameters(List originals) { + super(originals); + } + + public CassandraParameters(Method method) { + super(method); + } + + @Override + protected CassandraParameter createParameter(MethodParameter parameter) { + return new CassandraParameter(parameter); + } + + @Override + protected CassandraParameters createFrom(List parameters) { + return new CassandraParameters(parameters); + } + + class CassandraParameter extends Parameter { + + protected CassandraParameter(MethodParameter parameter) { + super(parameter); + } + + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessor.java new file mode 100644 index 000000000..f460c31d7 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessor.java @@ -0,0 +1,17 @@ +package org.springframework.data.cassandra.repository.query; + +import org.springframework.data.repository.query.ParametersParameterAccessor; + +public class CassandraParametersParameterAccessor extends ParametersParameterAccessor implements + CassandraParameterAccessor { + + /** + * Creates a new {@link CassandraParametersParameterAccessor}. + * + * @param method must not be {@literal null}. + * @param values must not be {@@iteral null}. + */ + public CassandraParametersParameterAccessor(CassandraQueryMethod method, Object... values) { + super(method.getParameters(), values); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java new file mode 100644 index 000000000..a6ba28122 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java @@ -0,0 +1,220 @@ +/* + * Copyright 2010-2013 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.Collection; +import java.util.Iterator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.PersistentPropertyPath; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.util.Assert; + +import com.datastax.driver.core.querybuilder.Clause; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; + +/** + * Custom query creator to create Cassandra criteria. + */ +class CassandraQueryCreator extends AbstractQueryCreator { + + private static final Logger LOG = LoggerFactory.getLogger(CassandraQueryCreator.class); + private final CassandraParameterAccessor accessor; + + private final CassandraMappingContext context; + + /** + * Creates a new {@link CassandraQueryCreator} from the given {@link PartTree}, {@link ConvertingParameterAccessor} + * and {@link MappingContext}. + * + * @param tree + * @param accessor + * @param context + */ + public CassandraQueryCreator(PartTree tree, CassandraParameterAccessor accessor, CassandraMappingContext context) { + + super(tree, accessor); + + Assert.notNull(context); + + this.accessor = accessor; + this.context = context; + } + + @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; + } + + @Override + protected Clause and(Part part, Clause base, Iterator iterator) { + + if (base == null) { + return 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); + } + + @Override + protected Clause or(Clause base, Clause criteria) { + throw new InvalidDataAccessApiUsageException(String.format("Cassandra does not support an OR operator!")); + } + + @Override + protected Select complete(Clause criteria, Sort sort) { + + if (criteria == null) { + return null; + } + + Select select = QueryBuilder.select().all().from("TODO"); + select.where(criteria); + + if (LOG.isDebugEnabled()) { + LOG.debug("Created query " + select.getQueryString()); + } + + return select; + } + + private Clause from(Part part, CassandraPersistentProperty property, Clause criteria, Iterator 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); + default: + throw new UnsupportedCassandraQueryOperatorException(String.format("")); + } + } + + private boolean isSimpleComparisionPossible(Part part) { + + 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; + } + + throw new IllegalArgumentException(String.format("Expected parameter type of %s but got %s!", type, + parameter.getClass())); + } + + private Object[] nextAsArray(Iterator iterator, CassandraPersistentProperty property) { + Object next = iterator.next(); // TODO nextConverted(property); + + if (next instanceof Collection) { + return ((Collection) next).toArray(); + } else if (next.getClass().isArray()) { + return (Object[]) next; + } + + return new Object[] { next }; + } +} 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 new file mode 100644 index 000000000..fcba6a282 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java @@ -0,0 +1,156 @@ +package org.springframework.data.cassandra.repository.query; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.repository.core.RepositoryMetadata; +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; + +public class CassandraQueryMethod extends QueryMethod { + + // TODO: double-check this list + public static final List> ALLOWED_PARAMETER_TYPES = Collections.unmodifiableList(Arrays + .asList(new Class[] { String.class, CharSequence.class, char.class, Character.class, char[].class, long.class, + Long.class, boolean.class, Boolean.class, BigDecimal.class, BigInteger.class, double.class, Double.class, + float.class, Float.class, InetAddress.class, Date.class, UUID.class, })); + + public static boolean isMapOfCharSequenceToObject(TypeInformation type) { + + if (!type.isMap()) { + return false; + } + + TypeInformation keyType = type.getComponentType(); + TypeInformation valueType = type.getMapValueType(); + + return ClassUtils.isAssignable(CharSequence.class, keyType.getType()) && Object.class.equals(valueType.getType()); + } + + protected Method method; + protected CassandraMappingContext mappingContext; + protected Query query; + protected String queryString; + protected boolean queryCached = false; + + public CassandraQueryMethod(Method method, RepositoryMetadata metadata, CassandraMappingContext mappingContext) { + + super(method, metadata); + + verify(method, metadata); + + this.method = method; + + Assert.notNull(mappingContext, "MappingContext must not be null!"); + this.mappingContext = mappingContext; + } + + public void verify(Method method, RepositoryMetadata metadata) { + + // TODO: support Page & Slice queries + if (isSliceQuery() || isPageQuery()) { + throw new InvalidDataAccessApiUsageException("neither slice nor page queries are supported yet"); + } + + Set> offendingTypes = new HashSet>(); + for (Class type : method.getParameterTypes()) { + if (!ALLOWED_PARAMETER_TYPES.contains(type)) { + offendingTypes.add(type); + } + } + + if (offendingTypes.size() > 0) { + throw new IllegalArgumentException(String.format( + "encountered unsupported query parameter type%s [%s] in method %s", offendingTypes.size() == 1 ? "" : "s", + StringUtils.arrayToCommaDelimitedString(new ArrayList>(offendingTypes).toArray()), method)); + } + } + + @Override + protected CassandraParameters createParameters(Method method) { + 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 = method.getAnnotation(Query.class); + queryCached = true; + } + return query; + } + + /** + * Returns whether the method has an annotated query. + */ + public boolean hasAnnotatedQuery() { + return getAnnotatedQuery() != null; + } + + /** + * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found + * nor the attribute was specified. + */ + public String getAnnotatedQuery() { + + if (!queryCached) { + queryString = (String) AnnotationUtils.getValue(getQueryAnnotation()); + queryString = StringUtils.hasText(queryString) ? queryString : null; + } + + return queryString; + } + + public TypeInformation getReturnType() { + return ClassTypeInformation.fromReturnTypeOf(method); + } + + public boolean isResultSetQuery() { + return ResultSet.class.isAssignableFrom(method.getReturnType()); + } + + public boolean isSingleEntityQuery() { + return ClassUtils.isAssignable(getDomainClass(), method.getReturnType()); + } + + public boolean isCollectionOfEntityQuery() { + return isQueryForEntity() && isCollectionQuery(); + } + + public boolean isMapOfCharSequenceToObjectQuery() { + + return isMapOfCharSequenceToObject(getReturnType()); + } + + public boolean isListOfMapOfCharSequenceToObject() { + + TypeInformation type = getReturnType(); + if (!ClassUtils.isAssignable(List.class, type.getType())) { + return false; + } + + return isMapOfCharSequenceToObject(type.getComponentType()); + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..837d88343 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2013 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.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * {@link RepositoryQuery} implementation for Cassandra. + * + */ +public class PartTreeCassandraQuery extends AbstractCassandraQuery { + + private final PartTree tree; + private final CassandraMappingContext context; + + /** + * 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}. + */ + public PartTreeCassandraQuery(CassandraQueryMethod method, CassandraOperations cassandraOperations) { + + super(method, cassandraOperations); + this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); + this.context = cassandraOperations.getConverter().getMappingContext(); + } + + /** + * Return the {@link PartTree} backing the query. + * + * @return the tree + */ + public PartTree getTree() { + return tree; + } + + @Override + protected String createQuery(CassandraParameterAccessor accessor) { + + CassandraQueryCreator creator = new CassandraQueryCreator(tree, accessor, context); + return creator.createQuery().getQueryString(); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java new file mode 100644 index 000000000..133bf72bb --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java @@ -0,0 +1,50 @@ +package org.springframework.data.cassandra.repository.query; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.cassandra.core.CassandraOperations; + +public class StringBasedCassandraQuery extends AbstractCassandraQuery { + + private static final Pattern PLACEHOLDER = Pattern.compile("\\?(\\d+)"); + private static final Logger LOG = LoggerFactory.getLogger(StringBasedCassandraQuery.class); + + protected String query; + + public StringBasedCassandraQuery(String query, CassandraQueryMethod queryMethod, CassandraOperations operations) { + + super(queryMethod, operations); + + this.query = query; + } + + public StringBasedCassandraQuery(CassandraQueryMethod queryMethod, CassandraOperations operations) { + this(queryMethod.getAnnotatedQuery(), queryMethod, operations); + } + + @Override + public String createQuery(CassandraParameterAccessor accessor) { + return replacePlaceholders(query, accessor); + } + + private String replacePlaceholders(String input, CassandraParameterAccessor accessor) { + + Matcher matcher = PLACEHOLDER.matcher(input); + String result = input; + + while (matcher.find()) { + String group = matcher.group(); + int index = Integer.parseInt(matcher.group(1)); + result = result.replace(group, getParameterWithIndex(accessor, index).toString()); + } + + return result; + } + + private Object getParameterWithIndex(CassandraParameterAccessor accessor, int index) { + return accessor.getBindableValue(index); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/UnsupportedCassandraQueryOperatorException.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/UnsupportedCassandraQueryOperatorException.java new file mode 100644 index 000000000..ef2475f13 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/UnsupportedCassandraQueryOperatorException.java @@ -0,0 +1,17 @@ +package org.springframework.data.cassandra.repository.query; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +public class UnsupportedCassandraQueryOperatorException extends InvalidDataAccessApiUsageException { + + public UnsupportedCassandraQueryOperatorException(String msg) { + super(msg); + // TODO Auto-generated constructor stub + } + + public UnsupportedCassandraQueryOperatorException(String msg, Throwable cause) { + super(msg, cause); + // TODO Auto-generated constructor stub + } + +} 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 fc03dcfce..0c5ab8ed3 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 @@ -16,16 +16,23 @@ 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.CassandraTemplate; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; -import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; import org.springframework.data.cassandra.repository.TypedIdCassandraRepository; import org.springframework.data.cassandra.repository.query.CassandraEntityInformation; -import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.cassandra.repository.query.CassandraQueryMethod; +import org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery; import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; /** @@ -38,7 +45,7 @@ import org.springframework.util.Assert; public class CassandraRepositoryFactory extends RepositoryFactorySupport { private final CassandraTemplate cassandraTemplate; - private final MappingContext, CassandraPersistentProperty> mappingContext; + private final CassandraMappingContext mappingContext; /** * Creates a new {@link MongoRepositoryFactory} with the given {@link MongoOperations}. @@ -51,6 +58,9 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { this.cassandraTemplate = cassandraTemplate; this.mappingContext = cassandraTemplate.getConverter().getMappingContext(); + + // TODO: remove when supporting declarative query methods + setQueryLookupStrategyKey(QueryLookupStrategy.Key.USE_DECLARED_QUERY); } @Override @@ -82,4 +92,28 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { return new MappingCassandraEntityInformation((CassandraPersistentEntity) entity, cassandraTemplate.getConverter()); } + + @Override + protected QueryLookupStrategy getQueryLookupStrategy(Key key) { + return new CassandraQueryLookupStrategy(); + } + + private class CassandraQueryLookupStrategy implements QueryLookupStrategy { + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, NamedQueries namedQueries) { + + CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, mappingContext); + String namedQueryName = queryMethod.getNamedQueryName(); + + if (namedQueries.hasQuery(namedQueryName)) { + String namedQuery = namedQueries.getQuery(namedQueryName); + return new StringBasedCassandraQuery(namedQuery, queryMethod, cassandraTemplate); + } else if (queryMethod.hasAnnotatedQuery()) { + return new StringBasedCassandraQuery(queryMethod, cassandraTemplate); + } else { + throw new InvalidDataAccessApiUsageException("declarative query methods are a todo"); + } + } + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/Person.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/Person.java new file mode 100644 index 000000000..b9213795d --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/Person.java @@ -0,0 +1,101 @@ +/* + * Copyright 2010-2013 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.querymethods.declared; + +import java.util.Date; + +import org.springframework.cassandra.core.PrimaryKeyType; +import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; +import org.springframework.data.cassandra.mapping.Table; + +/** + * Sample domain class. + */ +@Table +public class Person { + + @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 0) + private String lastname; + + @PrimaryKeyColumn(type = PrimaryKeyType.CLUSTERED, ordinal = 1) + private String firstname; + + private String nickname; + + private Date birthDate; + + private int numberOfChildren; + + private boolean cool; + + // TODO: private UUID uuid = UUID.randomUUID(); + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public Date getBirthDate() { + return new Date(birthDate.getTime()); + } + + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate == null ? null : new Date(birthDate.getTime()); + } + + public int getNumberOfChildren() { + return numberOfChildren; + } + + public void setNumberOfChildren(int numberOfChildren) { + this.numberOfChildren = numberOfChildren; + } + + public boolean isCool() { + return cool; + } + + public void setCool(boolean cool) { + this.cool = cool; + } + + // public UUID getUuid() { + // return uuid; + // } + // + // public void setUuid(UUID uuid) { + // this.uuid = uuid; + // } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/QueryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/QueryIntegrationTests.java new file mode 100644 index 000000000..08c932b16 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/QueryIntegrationTests.java @@ -0,0 +1,251 @@ +package org.springframework.data.cassandra.test.integration.querymethods.declared; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.test.integration.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.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +public abstract class QueryIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { + + 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 + PersonRepository r; + + @Before + public void before() { + deleteAllEntities(); + } + + @Test + public void testListMethodSingleResult() { + + Person saved = new Person(); + saved.setFirstname(uuid()); + saved.setLastname(uuid()); + + saved = r.save(saved); + + List results = r.findFolksWithLastnameAsList(saved.getLastname()); + + assertNotNull(results); + assertTrue(results.size() == 1); + Person found = results.iterator().next(); + assertNotNull(found); + assertEquals(found.getLastname(), saved.getLastname()); + assertEquals(found.getFirstname(), saved.getFirstname()); + } + + @Test + public void testListMethodMultipleResults() { + + Person saved = new Person(); + saved.setFirstname("a"); + saved.setLastname(uuid()); + + saved = r.save(saved); + + Person saved2 = new Person(); + saved2.setFirstname("b"); + saved2.setLastname(saved.getLastname()); + + saved2 = r.save(saved2); + + List results = r.findFolksWithLastnameAsList(saved.getLastname()); + + assertNotNull(results); + assertTrue(results.size() == 2); + boolean first = true; + for (Person person : results) { + assertNotNull(person); + assertEquals(saved.getLastname(), person.getLastname()); + assertEquals(first ? saved.getFirstname() : saved2.getFirstname(), person.getFirstname()); + first = false; + } + } + + @Test + public void testListOfMapOfStringToObjectMethodSingleResult() { + + Person saved = new Person(); + saved.setFirstname(uuid()); + saved.setLastname(uuid()); + + saved = r.save(saved); + + List> results = r.findFolksWithLastnameAsListOfMapOfStringToObject(saved.getLastname()); + + assertNotNull(results); + assertTrue(results.size() == 1); + Map found = results.iterator().next(); + assertNotNull(found); + assertEquals(found.get("lastname"), saved.getLastname()); + assertEquals(found.get("firstname"), saved.getFirstname()); + } + + @Test + public void testEntityMethodResult() { + + Person saved = new Person(); + saved.setFirstname(uuid()); + saved.setLastname(uuid()); + + saved = r.save(saved); + + Person found = r.findSingle(saved.getLastname(), saved.getFirstname()); + + assertNotNull(found); + assertEquals(found.getLastname(), saved.getLastname()); + assertEquals(found.getFirstname(), saved.getFirstname()); + } + + @Test + public void testListOfMapOfStringToObjectMethodMultipleResults() { + + Person saved = new Person(); + saved.setFirstname("a"); + saved.setLastname(uuid()); + + saved = r.save(saved); + + Person saved2 = new Person(); + saved2.setFirstname("b"); + saved2.setLastname(saved.getLastname()); + + saved2 = r.save(saved2); + + Collection results = r.findFolksWithLastnameAsList(saved.getLastname()); + + assertNotNull(results); + assertTrue(results.size() == 2); + boolean first = true; + for (Person person : results) { + assertNotNull(person); + assertEquals(saved.getLastname(), person.getLastname()); + assertEquals(first ? saved.getFirstname() : saved2.getFirstname(), person.getFirstname()); + first = false; + } + } + + @Test + public void testStringMethodResult() { + + Person saved = new Person(); + saved.setFirstname(uuid()); + saved.setLastname(uuid()); + saved.setNickname(uuid()); + + saved = r.save(saved); + + String nickname = r.findSingleNickname(saved.getLastname(), saved.getFirstname()); + + assertNotNull(nickname); + assertEquals(saved.getNickname(), nickname); + } + + @Test + public void testBooleanMethodResult() { + + Person saved = new Person(); + saved.setFirstname(uuid()); + saved.setLastname(uuid()); + saved.setCool(true); + + saved = r.save(saved); + + boolean value = r.findSingleCool(saved.getLastname(), saved.getFirstname()); + + assertEquals(saved.isCool(), value); + } + + @Test + public void testDateMethodResult() { + + Person saved = new Person(); + saved.setFirstname(uuid()); + saved.setLastname(uuid()); + saved.setBirthDate(new Date()); + + saved = r.save(saved); + + Date value = r.findSingleBirthdate(saved.getLastname(), saved.getFirstname()); + + assertEquals(saved.getBirthDate(), value); + } + + @Test + public void testIntMethodResult() { + + Person saved = new Person(); + saved.setFirstname(uuid()); + saved.setLastname(uuid()); + saved.setNumberOfChildren(1); + + saved = r.save(saved); + + int value = r.findSingleNumberOfChildren(saved.getLastname(), saved.getFirstname()); + + assertEquals(saved.getNumberOfChildren(), value); + } + + // TODO: @Test + // public void testUuidMethodResult() { + // + // Person saved = new Person(); + // saved.setFirstname(uuid()); + // saved.setLastname(uuid()); + // saved.setUuid(UUID.randomUUID()); + // + // saved = r.save(saved); + // + // UUID value = r.findSingleUuid(saved.getLastname(), saved.getFirstname()); + // + // assertEquals(saved.getUuid(), value); + // } + + @Test + public void testArrayMethodSingleResult() { + + Person saved = new Person(); + saved.setFirstname(uuid()); + saved.setLastname(uuid()); + + saved = r.save(saved); + + Person[] results = r.findFolksWithLastnameAsArray(saved.getLastname()); + + assertNotNull(results); + assertTrue(results.length == 1); + Person found = results[0]; + assertNotNull(found); + assertEquals(found.getLastname(), saved.getLastname()); + assertEquals(found.getFirstname(), saved.getFirstname()); + } + +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/StringBasedCassandraQueryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/StringBasedCassandraQueryIntegrationTests.java new file mode 100644 index 000000000..911214815 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/StringBasedCassandraQueryIntegrationTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2011-2013 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.querymethods.declared; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; + +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.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.cassandra.repository.query.CassandraParametersParameterAccessor; +import org.springframework.data.cassandra.repository.query.CassandraQueryMethod; +import org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery; +import org.springframework.data.repository.core.RepositoryMetadata; + +import com.datastax.driver.core.SimpleStatement; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; + +/** + * Unit tests for {@link StringBasedCassandraQuery}. + */ +@RunWith(MockitoJUnitRunner.class) +public class StringBasedCassandraQueryIntegrationTests { + + @Mock + CassandraOperations operations; + @Mock + RepositoryMetadata metadata; + + CassandraConverter converter; + + @Before + public void setUp() { + + when(operations.getConverter()).thenReturn(converter); + + this.converter = new MappingCassandraConverter(new BasicCassandraMappingContext()); + } + + @Test + public void bindsSimplePropertyCorrectly() throws Exception { + + Method method = SampleRepository.class.getMethod("findByLastname", String.class); + CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, converter.getMappingContext()); + StringBasedCassandraQuery cassandraQuery = new StringBasedCassandraQuery(queryMethod, operations); + CassandraParametersParameterAccessor accesor = new CassandraParametersParameterAccessor(queryMethod, "Matthews"); + + String stringQuery = cassandraQuery.createQuery(accesor); + SimpleStatement actual = new SimpleStatement(stringQuery); + + String table = Person.class.getSimpleName().toLowerCase(); + Select expected = QueryBuilder.select().all().from(table); + expected.where(QueryBuilder.eq("lastname", "Matthews")); + + assertThat(actual.getQueryString(), is(expected.getQueryString())); + } + + @Test + public void bindsMultipleParametersCorrectly() throws Exception { + + Method method = SampleRepository.class.getMethod("findByLastnameAndFirstname", String.class, String.class); + CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, converter.getMappingContext()); + StringBasedCassandraQuery cassandraQuery = new StringBasedCassandraQuery(queryMethod, operations); + CassandraParametersParameterAccessor accesor = new CassandraParametersParameterAccessor(queryMethod, "Matthews", + "John"); + + String stringQuery = cassandraQuery.createQuery(accesor); + SimpleStatement actual = new SimpleStatement(stringQuery); + + String table = Person.class.getSimpleName().toLowerCase(); + Select expected = QueryBuilder.select().all().from(table); + expected.where(QueryBuilder.eq("lastname", "Matthews")).and(QueryBuilder.eq("firstname", "John")); + + assertThat(actual.getQueryString(), is(expected.getQueryString())); + } + + private interface SampleRepository { + + @Query("SELECT * FROM person WHERE lastname='?0';") + Person findByLastname(String lastname); + + @Query("SELECT * FROM person WHERE lastname='?0' AND firstname='?1';") + Person findByLastnameAndFirstname(String lastname, String firstname); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java new file mode 100644 index 000000000..71f881843 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java @@ -0,0 +1,54 @@ +package org.springframework.data.cassandra.test.integration.querymethods.declared.anno; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.springframework.data.cassandra.repository.Query; +import org.springframework.data.cassandra.test.integration.querymethods.declared.Person; +import org.springframework.data.cassandra.test.integration.querymethods.declared.base.PersonRepository; +import org.springframework.data.cassandra.test.integration.querymethods.declared.named.PersonRepositoryWithNamedQueries; + +import com.datastax.driver.core.ResultSet; + +/** + * we extend {@link PersonRepositoryWithNamedQueries} here just to keep the test codebase in sync. + */ +public interface PersonRepositoryWithQueryAnnotations extends PersonRepository { + + @Override + @Query("select * from person where lastname = '?0'") + List findFolksWithLastnameAsList(String lastname); + + @Override + @Query("select * from person where lastname = '?0'") + ResultSet findFolksWithLastnameAsResultSet(String last); + + @Override + @Query("select * from person where lastname = '?0'") + Person[] findFolksWithLastnameAsArray(String lastname); + + @Override + @Query("select * from person where lastname = '?0' and firstname = '?1'") + Person findSingle(String last, String first); + + @Override + @Query("select * from person where lastname = '?0'") + List> findFolksWithLastnameAsListOfMapOfStringToObject(String last); + + @Override + @Query("select nickname from person where lastname = '?0' and firstname = '?1'") + String findSingleNickname(String last, String first); + + @Override + @Query("select birthdate from person where lastname = '?0' and firstname = '?1'") + Date findSingleBirthdate(String last, String first); + + @Override + @Query("select cool from person where lastname = '?0' and firstname = '?1'") + boolean findSingleCool(String last, String first); + + @Override + @Query("select numberofchildren from person where lastname = '?0' and firstname = '?1'") + int findSingleNumberOfChildren(String last, String first); +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/anno/QueryAnnotationIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/anno/QueryAnnotationIntegrationTests.java new file mode 100644 index 000000000..df6841ff7 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/anno/QueryAnnotationIntegrationTests.java @@ -0,0 +1,15 @@ +package org.springframework.data.cassandra.test.integration.querymethods.declared.anno; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.data.cassandra.test.integration.querymethods.declared.QueryIntegrationTests; +import org.springframework.test.context.ContextConfiguration; + +@ContextConfiguration +public class QueryAnnotationIntegrationTests extends QueryIntegrationTests { + + @Configuration + @EnableCassandraRepositories(basePackageClasses = PersonRepositoryWithQueryAnnotations.class) + public static class Config extends QueryIntegrationTests.Config { + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/base/PersonRepository.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/base/PersonRepository.java new file mode 100644 index 000000000..4a4d97e92 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/base/PersonRepository.java @@ -0,0 +1,33 @@ +package org.springframework.data.cassandra.test.integration.querymethods.declared.base; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.springframework.data.cassandra.repository.CassandraRepository; +import org.springframework.data.cassandra.test.integration.querymethods.declared.Person; +import org.springframework.data.repository.NoRepositoryBean; + +import com.datastax.driver.core.ResultSet; + +@NoRepositoryBean +public interface PersonRepository extends CassandraRepository { + + List findFolksWithLastnameAsList(String lastname); + + ResultSet findFolksWithLastnameAsResultSet(String last); + + Person[] findFolksWithLastnameAsArray(String lastname); + + Person findSingle(String last, String first); + + List> findFolksWithLastnameAsListOfMapOfStringToObject(String last); + + String findSingleNickname(String last, String first); + + Date findSingleBirthdate(String last, String first); + + boolean findSingleCool(String last, String first); + + int findSingleNumberOfChildren(String last, String first); +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/named/NamedQueryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/named/NamedQueryIntegrationTests.java new file mode 100644 index 000000000..0673f347c --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/named/NamedQueryIntegrationTests.java @@ -0,0 +1,15 @@ +package org.springframework.data.cassandra.test.integration.querymethods.declared.named; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.data.cassandra.test.integration.querymethods.declared.QueryIntegrationTests; +import org.springframework.test.context.ContextConfiguration; + +@ContextConfiguration +public class NamedQueryIntegrationTests extends QueryIntegrationTests { + + @Configuration + @EnableCassandraRepositories(basePackageClasses = PersonRepositoryWithNamedQueries.class, namedQueriesLocation = "classpath:META-INF/PersonRepositoryWithNamedQueries.properties") + public static class Config extends QueryIntegrationTests.Config { + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/named/PersonRepositoryWithNamedQueries.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/named/PersonRepositoryWithNamedQueries.java new file mode 100644 index 000000000..1ec86d130 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/declared/named/PersonRepositoryWithNamedQueries.java @@ -0,0 +1,6 @@ +package org.springframework.data.cassandra.test.integration.querymethods.declared.named; + +import org.springframework.data.cassandra.test.integration.querymethods.declared.base.PersonRepository; + +public interface PersonRepositoryWithNamedQueries extends PersonRepository { +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/derived/TODO.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/derived/TODO.java new file mode 100644 index 000000000..61673e3ec --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/querymethods/derived/TODO.java @@ -0,0 +1,5 @@ +package org.springframework.data.cassandra.test.integration.querymethods.derived; + +// TODO +public class TODO { +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepository.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepository.java index 293404960..f0d8eb100 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepository.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepository.java @@ -21,7 +21,9 @@ import org.springframework.data.cassandra.repository.TypedIdCassandraRepository; * Sample repository managing {@link User} entities. * * @author Alex Shvid - * + * @author Matthew T. Adams */ public interface UserRepository extends TypedIdCassandraRepository { + + String findByNamedQuery(String username); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryIntegrationTests.java index df98ac254..2887374e9 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryIntegrationTests.java @@ -93,6 +93,12 @@ public class UserRepositoryIntegrationTests { setUp(); } + public void findByNamedQuery() { + String name = repository.findByNamedQuery("bob"); + Assert.assertNotNull(name); + Assert.assertEquals("Bob", name); + } + public void findsUserById() throws Exception { User user = repository.findOne(bob.getUsername()); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryIntegrationTestsDelegator.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryIntegrationTestsDelegator.java new file mode 100644 index 000000000..2d75e5d16 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryIntegrationTestsDelegator.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2014 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; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Base class for Java config tests for {@link UserRepository}. + * + * @author Matthew T. Adams + */ +@RunWith(SpringJUnit4ClassRunner.class) +public abstract class UserRepositoryIntegrationTestsDelegator extends + AbstractSpringDataEmbeddedCassandraIntegrationTest { + + @Autowired + protected UserRepository repository; + + @Autowired + protected CassandraOperations template; + + UserRepositoryIntegrationTests tests; + + @Before + public void before() { + tests = new UserRepositoryIntegrationTests(repository, template); + tests.before(); + } + + @Test + public void findByNamedQuery() { + tests.findByNamedQuery(); + } + + @Test + public void findsUserById() throws Exception { + tests.findsUserById(); + } + + @Test + public void findsAll() throws Exception { + tests.findsAll(); + } + + @Test + public void findsAllWithGivenIds() { + tests.findsAllWithGivenIds(); + } + + @Test + public void deletesUserCorrectly() throws Exception { + tests.deletesUserCorrectly(); + } + + @Test + public void deletesUserByIdCorrectly() { + tests.deletesUserByIdCorrectly(); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryJavaConfigIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryJavaConfigIntegrationTests.java index 992dd8ec0..6f6f43a0a 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryJavaConfigIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryJavaConfigIntegrationTests.java @@ -15,26 +15,18 @@ */ package org.springframework.data.cassandra.test.integration.repository; -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.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; -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; /** - * Base class for Java config tests for {@link UserRepository}. + * Java config tests for {@link UserRepository}. * * @author Matthew T. Adams */ -@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration -public class UserRepositoryJavaConfigIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { +public class UserRepositoryJavaConfigIntegrationTests extends UserRepositoryIntegrationTestsDelegator { @Configuration @EnableCassandraRepositories(basePackageClasses = UserRepository.class) @@ -51,42 +43,4 @@ public class UserRepositoryJavaConfigIntegrationTests extends AbstractSpringData } } - @Autowired - protected UserRepository repository; - - @Autowired - protected CassandraOperations template; - - UserRepositoryIntegrationTests tests; - - @Before - public void before() { - tests = new UserRepositoryIntegrationTests(repository, template); - tests.before(); - } - - @Test - public void findsUserById() throws Exception { - tests.findsUserById(); - } - - @Test - public void findsAll() throws Exception { - tests.findsAll(); - } - - @Test - public void findsAllWithGivenIds() { - tests.findsAllWithGivenIds(); - } - - @Test - public void deletesUserCorrectly() throws Exception { - tests.deletesUserCorrectly(); - } - - @Test - public void deletesUserByIdCorrectly() { - tests.deletesUserByIdCorrectly(); - } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryXmlConfigIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryXmlConfigIntegrationTests.java index 2b8915299..1b9868394 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryXmlConfigIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/UserRepositoryXmlConfigIntegrationTests.java @@ -15,60 +15,13 @@ */ package org.springframework.data.cassandra.test.integration.repository; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.cassandra.core.CassandraOperations; -import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** - * Base class for xml config tests for {@link UserRepository}. + * xml config tests for {@link UserRepository}. * * @author Matthew T. Adams */ -@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration -public class UserRepositoryXmlConfigIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { - - @Autowired - protected UserRepository repository; - - @Autowired - protected CassandraOperations template; - - UserRepositoryIntegrationTests tests; - - @Before - public void before() { - tests = new UserRepositoryIntegrationTests(repository, template); - tests.before(); - } - - @Test - public void findsUserById() throws Exception { - tests.findsUserById(); - } - - @Test - public void findsAll() throws Exception { - tests.findsAll(); - } - - @Test - public void findsAllWithGivenIds() { - tests.findsAllWithGivenIds(); - } - - @Test - public void deletesUserCorrectly() throws Exception { - tests.deletesUserCorrectly(); - } - - @Test - public void deletesUserByIdCorrectly() { - tests.deletesUserByIdCorrectly(); - } +public class UserRepositoryXmlConfigIntegrationTests extends UserRepositoryIntegrationTestsDelegator { } diff --git a/spring-data-cassandra/src/test/resources/META-INF/PersonRepositoryWithNamedQueries.properties b/spring-data-cassandra/src/test/resources/META-INF/PersonRepositoryWithNamedQueries.properties new file mode 100644 index 000000000..6f9ad4229 --- /dev/null +++ b/spring-data-cassandra/src/test/resources/META-INF/PersonRepositoryWithNamedQueries.properties @@ -0,0 +1,9 @@ +Person.findFolksWithLastnameAsList=select * from person where lastname = '?0' +Person.findFolksWithLastnameAsResultSet=select * from person where lastname = '?0' +Person.findFolksWithLastnameAsArray=select * from person where lastname = '?0' +Person.findSingle=select * from person where lastname = '?0' and firstname = '?1' +Person.findFolksWithLastnameAsListOfMapOfStringToObject=select * from person where lastname = '?0' +Person.findSingleNickname=select nickname from person where lastname = '?0' and firstname = '?1' +Person.findSingleBirthdate=select birthdate from person where lastname = '?0' and firstname = '?1' +Person.findSingleCool=select cool from person where lastname = '?0' and firstname = '?1' +Person.findSingleNumberOfChildren=select numberofchildren from person where lastname = '?0' and firstname = '?1' diff --git a/spring-data-cassandra/src/test/resources/META-INF/cassandra-named-queries.properties b/spring-data-cassandra/src/test/resources/META-INF/cassandra-named-queries.properties index f8e240021..76c051295 100644 --- a/spring-data-cassandra/src/test/resources/META-INF/cassandra-named-queries.properties +++ b/spring-data-cassandra/src/test/resources/META-INF/cassandra-named-queries.properties @@ -1 +1 @@ -User.findByNamedQuery=SELECT firstName FROM table WHERE firstName=?0 +User.findByNamedQuery=SELECT firstname FROM users WHERE username='?0' diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/UserRepositoryXmlConfigIntegrationTests-context.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/UserRepositoryXmlConfigIntegrationTests-context.xml index 418d3d343..a03322f83 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/UserRepositoryXmlConfigIntegrationTests-context.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/test/integration/repository/UserRepositoryXmlConfigIntegrationTests-context.xml @@ -16,7 +16,7 @@ entity-base-packages="org.springframework.data.cassandra.test.integration.repository"> - +