From 66a69c0926b9291a7f0ef501136835e6964a1469 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 17 Jun 2016 15:36:26 +0200 Subject: [PATCH] DATACASS-296 - Use CustomConversions for JSR-310, Joda and ThreeTen Backport types. We now allow registering CustomConversions to register custom converters (read and write targets). CustomConversions registers by default JSR-310, Joda Time and ThreeTen backport type converters. These types (LocalDate, LocalDateTime) can be used in domain classes and with query method arguments. Date-only types map to Cassandra's date type, Date and time types map to Cassandra's timestamp type. Type mapping works for singular and collection types (List and Set). ``` @Table public Person @Id String id; java.time.LocalDate date; @CassandraType(type = Name.TIMESTAMP) java.time.LocalDate timestamp; List timestamps; } ``` Original pull request: #68. --- pom.xml | 21 +- spring-cql/template.mf | 19 +- spring-data-cassandra/pom.xml | 18 +- .../java/AbstractCassandraConfiguration.java | 53 ++- .../convert/CassandraJodaTimeConverters.java | 130 ++++++ .../convert/CassandraJsr310Converters.java | 93 +++++ .../CassandraThreeTenBackPortConverters.java | 99 +++++ .../cassandra/convert/CustomConversions.java | 22 +- .../convert/MappingCassandraConverter.java | 167 ++++++-- .../mapping/BasicCassandraMappingContext.java | 122 +++++- .../BasicCassandraPersistentProperty.java | 53 +-- .../mapping/CassandraMappingContext.java | 37 +- .../mapping/CassandraPersistentProperty.java | 14 +- .../mapping/CassandraSimpleTypeHolder.java | 2 + .../data/cassandra/mapping/CassandraType.java | 5 +- .../query/AbstractCassandraQuery.java | 2 +- .../query/CassandraParameterAccessor.java | 48 ++- .../repository/query/CassandraParameters.java | 81 +++- .../CassandraParametersParameterAccessor.java | 60 ++- .../query/CassandraQueryMethod.java | 158 +++---- .../query/ConvertingParameterAccessor.java | 178 ++++++++ .../query/StringBasedCassandraQuery.java | 77 +++- .../data/cassandra/RowMockUtil.java | 156 +++++++ .../ConverterRegistrationUnitTests.java | 5 +- .../convert/CustomConversionsUnitTests.java | 46 +++ .../MappingCassandraConverterUnitTests.java | 382 ++++++++++++++++- .../cassandra/domain/AllPossibleTypes.java | 21 + .../data/cassandra/domain/Person.java | 32 ++ ...BasicCassandraMappingContextUnitTests.java | 385 ++++++++++++++++++ .../mapping/MappingContextUnitTests.java | 241 ++++++----- ...yMethodParameterTypesIntegrationTests.java | 232 +++++++++++ ...RepositoryReturnTypesIntegrationTests.java | 2 +- ...aParametersParameterAccessorUnitTests.java | 133 ++++++ .../query/CassandraParametersUnitTests.java | 101 +++++ .../ConvertingParameterAccessorUnitTests.java | 96 +++++ ...edCassandraQueryIntegrationUnitTests.java} | 45 +- .../config/SchemaActionIntegrationTests.java | 121 +++--- .../CustomConversionTests.java | 290 +++++++++++++ .../CassandraTypeMappingIntegrationTest.java | 196 +++++++++ .../querymethods/declared/Person.java | 10 +- .../declared/QueryIntegrationTests.java | 12 +- .../PersonRepositoryWithQueryAnnotations.java | 3 + .../declared/base/PersonRepository.java | 3 +- ...ersonRepositoryWithNamedQueries.properties | 3 + spring-data-cassandra/template.mf | 20 +- 45 files changed, 3544 insertions(+), 450 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraJodaTimeConverters.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraJsr310Converters.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraThreeTenBackPortConverters.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/RowMockUtil.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateTableSpecificationBasicCassandraMappingContextUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryQueryMethodParameterTypesIntegrationTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessorUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java rename spring-data-cassandra/src/test/java/org/springframework/data/cassandra/{test/integration/repository/querymethods/declared/StringBasedCassandraQueryIntegrationTests.java => repository/query/StringBasedCassandraQueryIntegrationUnitTests.java} (72%) create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/customconversion/CustomConversionTests.java diff --git a/pom.xml b/pom.xml index 50a87ddc8..1f6ae9734 100644 --- a/pom.xml +++ b/pom.xml @@ -124,6 +124,20 @@ true + + joda-time + joda-time + ${jodatime} + true + + + + org.threeten + threetenbp + ${threetenbp} + true + + org.apache.cassandra @@ -177,13 +191,6 @@ test - - joda-time - joda-time - ${jodatime} - test - - org.apache.commons commons-lang3 diff --git a/spring-cql/template.mf b/spring-cql/template.mf index a61324370..2c2bb8c07 100644 --- a/spring-cql/template.mf +++ b/spring-cql/template.mf @@ -5,6 +5,12 @@ Bundle-ManifestVersion: 2 Import-Package: sun.reflect;version="0";resolution:=optional Import-Template: + com.datastax.driver.core.*;resolution:="optional";version="[0.1.0, 1.0.0)", + com.google.common.*;resolution:="optional";version="[11.0.0, 20.0.0)", + javax.xml.transform.*;resolution:="optional";version="0", + org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.codehaus.jackson.*;resolution:="optional";version="[1.6, 2.0.0)", org.springframework.beans.*;version="[3.1.0, 4.0.0)", org.springframework.cache.*;version="[3.1.0, 4.0.0)", org.springframework.context.*;version="[3.1.0, 4.0.0)", @@ -16,15 +22,6 @@ Import-Template: org.springframework.transaction.support.*;version="[3.1.0, 4.0.0)", org.springframework.data.*;version="[1.5.0, 2.0.0)", org.springframework.expression.*;version="[3.1.0, 4.0.0)", - org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, - org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", - org.w3c.dom.*;version="0", - javax.xml.transform.*;resolution:="optional";version="0", - com.datastax.driver.core.*;resolution:="optional";version="[0.1.0, 1.0.0)", - org.apache.cassandra.db.marshal.*;version="[1.2.0, 1.3.0)", + org.springframework.cassandra.*;version="[1.0.0,2.0.0)", org.slf4j.*;version="[1.5.0, 1.8.0)", - org.idevlab.rjc.*;resolution:="optional";version="[0.6.4, 0.6.4]", - org.apache.commons.pool.impl.*;resolution:="optional";version="[1.0.0, 3.0.0)", - org.codehaus.jackson.*;resolution:="optional";version="[1.6, 2.0.0)", - org.apache.commons.beanutils.*;resolution:="optional";version=1.8.5, - com.google.common.*;resolution:="optional";version="[11.0.0, 20.0.0)" \ No newline at end of file + org.w3c.dom.*;version="0" diff --git a/spring-data-cassandra/pom.xml b/spring-data-cassandra/pom.xml index 7c17a6ec6..40fa902fa 100644 --- a/spring-data-cassandra/pom.xml +++ b/spring-data-cassandra/pom.xml @@ -59,6 +59,18 @@ true + + joda-time + joda-time + true + + + + org.threeten + threetenbp + true + + javax.enterprise @@ -110,12 +122,6 @@ test - - joda-time - joda-time - test - - commons-io commons-io diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java index f13d79713..b0efd5edf 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/java/AbstractCassandraConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,18 @@ */ package org.springframework.data.cassandra.config.java; +import java.util.Collections; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.cassandra.config.java.AbstractClusterConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.config.CassandraEntityClassScanner; import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; import org.springframework.data.cassandra.config.SchemaAction; import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.CassandraAdminOperations; import org.springframework.data.cassandra.core.CassandraAdminTemplate; @@ -37,6 +41,7 @@ import org.springframework.data.mapping.context.MappingContext; * @author Alex Shvid * @author Matthew T. Adams * @author John Blum + * @author Mark Paluch */ @Configuration public abstract class AbstractCassandraConfiguration extends AbstractClusterConfiguration @@ -72,15 +77,44 @@ public abstract class AbstractCassandraConfiguration extends AbstractClusterConf mappingContext.setBeanClassLoader(beanClassLoader); mappingContext.setInitialEntitySet(CassandraEntityClassScanner.scan(getEntityBasePackages())); + CustomConversions customConversions = customConversions(); + + mappingContext.setCustomConversions(customConversions); + mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); + return mappingContext; } /** - * Return the {@link CassandraConverter} instance to convert Rows to Objects, Objects to BuiltStatements + * Register custom {@link Converter}s in a {@link CustomConversions} object if required. These + * {@link CustomConversions} will be registered with the {@link #cassandraConverter()} and {@link #cassandraMapping()} + * . Returns an empty {@link CustomConversions} instance by default. + * + * @return must not be {@literal null}. + * @since 1.5 + */ + @Bean + public CustomConversions customConversions() { + return new CustomConversions(Collections.emptyList()); + } + + /** + * Creates a {@link CassandraConverter} using the configured {@link #cassandraMapping()}. Will get + * {@link #customConversions()} applied. + * + * @see #customConversions() + * @see #cassandraMapping() + * @return + * @throws Exception */ @Bean public CassandraConverter cassandraConverter() throws Exception { - return new MappingCassandraConverter(cassandraMapping()); + + MappingCassandraConverter mappingCassandraConverter = new MappingCassandraConverter(cassandraMapping()); + + mappingCassandraConverter.setCustomConversions(customConversions()); + + return mappingCassandraConverter; } /** @@ -99,19 +133,22 @@ public abstract class AbstractCassandraConfiguration extends AbstractClusterConf } /** - * Base packages to scan for entities annotated with {@link Table} annotations. By default, returns the package - * name of {@literal this} (this.getClass().getPackage().getName()). - * - * This method must never return null. + * Base packages to scan for entities annotated with {@link Table} annotations. By default, returns the package name + * of {@literal this} (this.getClass().getPackage().getName()). This method must never return null. */ public String[] getEntityBasePackages() { return new String[] { getClass().getPackage().getName() }; } + /** + * Return the name of the keyspace to connect to. + * + * @return must not be {@literal null}. + */ protected abstract String getKeyspaceName(); /** - * The {@link SchemaAction} to perform at startup. Defaults to {@link SchemaAction#NONE}. + * The {@link SchemaAction} to perform at startup. Defaults to {@link SchemaAction#NONE}. */ public SchemaAction getSchemaAction() { return SchemaAction.NONE; diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraJodaTimeConverters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraJodaTimeConverters.java new file mode 100644 index 000000000..b7adec039 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraJodaTimeConverters.java @@ -0,0 +1,130 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.convert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.joda.time.DateMidnight; +import org.joda.time.LocalDate; +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.ClassUtils; + +/** + * Helper class to register JSR-310 specific {@link Converter} implementations to convert between Cassandra types in + * case the library is present on the classpath. + * + * @author Mark Paluch + * @since 1.5 + */ +@SuppressWarnings("deprecation") +public abstract class CassandraJodaTimeConverters { + + private static final boolean JODA_TIME_IS_PRESENT = ClassUtils.isPresent("org.joda.time.LocalDate", null); + + private CassandraJodaTimeConverters() {} + + /** + * Returns the converters to be registered. Will only return converters in case JodaTime is present on the class path. + * + * @return + */ + public static Collection> getConvertersToRegister() { + + if (!JODA_TIME_IS_PRESENT) { + return Collections.emptySet(); + } + + List> converters = new ArrayList>(); + converters.add(CassandraLocalDateToLocalDateConverter.INSTANCE); + converters.add(LocalDateToCassandraLocalDateConverter.INSTANCE); + + converters.add(CassandraLocalDateToDateMidnightConverter.INSTANCE); + converters.add(DateMidnightToCassandraLocalDateConverter.INSTANCE); + + return converters; + } + + /** + * Simple singleton to convert {@link com.datastax.driver.core.LocalDate}s to their {@link LocalDate} representation. + * + * @author Mark Paluch + */ + public static enum CassandraLocalDateToLocalDateConverter + implements Converter { + + INSTANCE; + + @Override + public LocalDate convert(com.datastax.driver.core.LocalDate source) { + return new LocalDate(source.getYear(), source.getMonth(), source.getDay()); + } + } + + /** + * Simple singleton to convert {@link LocalDate}s to their {@link com.datastax.driver.core.LocalDate} representation. + * + * @author Mark Paluch + */ + public static enum LocalDateToCassandraLocalDateConverter + implements Converter { + + INSTANCE; + + @Override + public com.datastax.driver.core.LocalDate convert(LocalDate source) { + return com.datastax.driver.core.LocalDate.fromYearMonthDay(source.getYear(), source.getMonthOfYear(), + source.getDayOfMonth()); + } + } + + /** + * Simple singleton to convert {@link com.datastax.driver.core.LocalDate}s to their {@link DateMidnight} + * representation. + * + * @author Mark Paluch + */ + public static enum CassandraLocalDateToDateMidnightConverter + implements Converter { + + INSTANCE; + + @Override + public DateMidnight convert(com.datastax.driver.core.LocalDate source) { + return new DateMidnight(source.getYear(), source.getMonth(), source.getDay()); + } + } + + /** + * Simple singleton to convert {@link DateMidnight}s to their {@link com.datastax.driver.core.LocalDate} + * representation. + * + * @author Mark Paluch + */ + public static enum DateMidnightToCassandraLocalDateConverter + implements Converter { + + INSTANCE; + + @Override + public com.datastax.driver.core.LocalDate convert(DateMidnight source) { + return com.datastax.driver.core.LocalDate.fromYearMonthDay(source.getYear(), source.getMonthOfYear(), + source.getDayOfMonth()); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraJsr310Converters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraJsr310Converters.java new file mode 100644 index 000000000..aa31617f6 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraJsr310Converters.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.convert; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.Jsr310Converters; +import org.springframework.util.ClassUtils; + +/** + * Helper class to register JodaTime specific {@link Converter} implementations in case the library is present on the + * classpath. + * + * @author Mark Paluch + * @since 1.5 + */ +@SuppressWarnings("Since15") +public abstract class CassandraJsr310Converters { + + private static final boolean JAVA_8_IS_PRESENT = ClassUtils.isPresent("java.time.LocalDateTime", + Jsr310Converters.class.getClassLoader()); + + private CassandraJsr310Converters() {} + + /** + * Returns the converters to be registered. Will only return converters in case we're running on Java 8. + * + * @return + */ + public static Collection> getConvertersToRegister() { + + if (!JAVA_8_IS_PRESENT) { + return Collections.emptySet(); + } + + List> converters = new ArrayList>(); + converters.add(CassandraLocalDateToLocalDateConverter.INSTANCE); + converters.add(LocalDateToCassandraLocalDateConverter.INSTANCE); + + return converters; + } + + /** + * Simple singleton to convert {@link com.datastax.driver.core.LocalDate}s to their {@link LocalDate} representation. + * + * @author Mark Paluch + */ + public static enum CassandraLocalDateToLocalDateConverter + implements Converter { + + INSTANCE; + + @Override + public LocalDate convert(com.datastax.driver.core.LocalDate source) { + return LocalDate.of(source.getYear(), source.getMonth(), source.getDay()); + } + } + + /** + * Simple singleton to convert {@link LocalDate}s to their {@link com.datastax.driver.core.LocalDate} representation. + * + * @author Mark Paluch + */ + public static enum LocalDateToCassandraLocalDateConverter + implements Converter { + + INSTANCE; + + @Override + public com.datastax.driver.core.LocalDate convert(LocalDate source) { + return com.datastax.driver.core.LocalDate.fromYearMonthDay(source.getYear(), source.getMonthValue(), + source.getDayOfMonth()); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraThreeTenBackPortConverters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraThreeTenBackPortConverters.java new file mode 100644 index 000000000..ad78fa5e5 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraThreeTenBackPortConverters.java @@ -0,0 +1,99 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.convert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ThreeTenBackPortConverters; +import org.springframework.util.ClassUtils; +import org.threeten.bp.Instant; +import org.threeten.bp.LocalDate; +import org.threeten.bp.LocalDateTime; +import org.threeten.bp.ZoneId; + +/** + * Helper class to register {@link Converter} implementations for the ThreeTen Backport project in case it's present on + * the classpath. + * + * @author Mark Paluch + * @see http://www.threeten.org/threetenbp + * @since 1.5 + */ +public abstract class CassandraThreeTenBackPortConverters { + + private static final boolean THREE_TEN_BACK_PORT_IS_PRESENT = ClassUtils.isPresent("org.threeten.bp.LocalDateTime", + ThreeTenBackPortConverters.class.getClassLoader()); + + private CassandraThreeTenBackPortConverters() {} + + /** + * Returns the converters to be registered. Will only return converters in case ThreeTen Backport is on the class + * path. + * + * @return + */ + public static Collection> getConvertersToRegister() { + + if (!THREE_TEN_BACK_PORT_IS_PRESENT) { + return Collections.emptySet(); + } + + List> converters = new ArrayList>(); + converters.add(CassandraLocalDateToLocalDateConverter.INSTANCE); + converters.add(LocalDateToCassandraLocalDateConverter.INSTANCE); + + return converters; + } + + /** + * Simple singleton to convert {@link com.datastax.driver.core.LocalDate}s to their {@link LocalDate} representation. + * + * @author Mark Paluch + */ + public static enum CassandraLocalDateToLocalDateConverter + implements Converter { + + INSTANCE; + + @Override + public LocalDate convert(com.datastax.driver.core.LocalDate source) { + return LocalDate.of(source.getYear(), source.getMonth(), source.getDay()); + } + } + + /** + * Simple singleton to convert {@link LocalDate}s to their {@link com.datastax.driver.core.LocalDate} representation. + * + * @author Mark Paluch + */ + public static enum LocalDateToCassandraLocalDateConverter + implements Converter { + + INSTANCE; + + @Override + public com.datastax.driver.core.LocalDate convert(LocalDate source) { + return com.datastax.driver.core.LocalDate.fromYearMonthDay(source.getYear(), source.getMonthValue(), + source.getDayOfMonth()); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java index f5a893acc..414998163 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java @@ -35,7 +35,10 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder; +import org.springframework.data.convert.JodaTimeConverters; +import org.springframework.data.convert.Jsr310Converters; import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.ThreeTenBackPortConverters; import org.springframework.data.convert.WritingConverter; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.CacheValue; @@ -59,7 +62,7 @@ public class CustomConversions { private final Set readingPairs; private final Set writingPairs; private final Set> customSimpleTypes; - private final SimpleTypeHolder simpleTypeHolder; + private final CassandraSimpleTypeHolder simpleTypeHolder; private final List converters; @@ -96,6 +99,14 @@ public class CustomConversions { toRegister.addAll(converters); toRegister.addAll(CassandraConverters.getConvertersToRegister()); + toRegister.addAll(CassandraJodaTimeConverters.getConvertersToRegister()); + toRegister.addAll(CassandraJsr310Converters.getConvertersToRegister()); + toRegister.addAll(CassandraThreeTenBackPortConverters.getConvertersToRegister()); + + toRegister.addAll(JodaTimeConverters.getConvertersToRegister()); + toRegister.addAll(Jsr310Converters.getConvertersToRegister()); + toRegister.addAll(ThreeTenBackPortConverters.getConvertersToRegister()); + for (Object c : toRegister) { registerConversion(c); } @@ -106,6 +117,15 @@ public class CustomConversions { this.simpleTypeHolder = new CassandraSimpleTypeHolder(); } + /** + * Returns the underlying {@link SimpleTypeHolder}. + * + * @return + */ + public SimpleTypeHolder getSimpleTypeHolder() { + return simpleTypeHolder; + } + /** * Returns whether the given type is considered to be simple. That means it's either a general simple type or we have * a writing {@link Converter} registered for a particular type. 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 feb36b09e..ae9968dc7 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 @@ -18,6 +18,7 @@ package org.springframework.data.cassandra.convert; import static org.springframework.data.cassandra.repository.support.BasicMapId.*; import java.io.Serializable; +import java.util.Collection; import java.util.Map; import org.slf4j.Logger; @@ -26,6 +27,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.CollectionFactory; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraMappingContext; @@ -45,6 +47,7 @@ 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.ObjectUtils; import com.datastax.driver.core.CodecRegistry; import com.datastax.driver.core.Row; @@ -71,15 +74,18 @@ import com.datastax.driver.core.querybuilder.Update; * @see org.springframework.data.convert.EntityWriter */ public class MappingCassandraConverter extends AbstractCassandraConverter - implements CassandraConverter, ApplicationContextAware, BeanClassLoaderAware { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - protected final CassandraMappingContext mappingContext; + implements CassandraConverter, ApplicationContextAware, BeanClassLoaderAware { protected ApplicationContext applicationContext; - protected SpELContext spELContext; + + protected final CassandraMappingContext mappingContext; + protected ClassLoader beanClassLoader; + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected SpELContext spELContext; + /** * Creates a new {@link MappingCassandraConverter} with a {@link BasicCassandraMappingContext}. */ @@ -96,7 +102,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter super(new DefaultConversionService()); - Assert.notNull(mappingContext); + Assert.notNull(mappingContext, "CassandraMappingContext must not be null"); this.mappingContext = mappingContext; this.spELContext = new SpELContext(RowReaderPropertyAccessor.INSTANCE); @@ -131,6 +137,9 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return readEntityFromRow(persistentEntity, row); } + /* (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; @@ -154,20 +163,19 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } protected void readPropertiesFromRow(final CassandraPersistentEntity entity, - final BasicCassandraRowValueProvider row, final PersistentPropertyAccessor accessor) { + final BasicCassandraRowValueProvider row, final PersistentPropertyAccessor propertyAccessor) { entity.doWithProperties(new PropertyHandler() { @Override - public void doWithPersistentProperty(CassandraPersistentProperty prop) { - MappingCassandraConverter.this.readPropertyFromRow(entity, prop, row, accessor); + public void doWithPersistentProperty(CassandraPersistentProperty property) { + MappingCassandraConverter.this.readPropertyFromRow(entity, property, row, propertyAccessor); } }); } - // TODO argh! too many return statements! - protected void readPropertyFromRow(final CassandraPersistentEntity entity, final CassandraPersistentProperty property, - final BasicCassandraRowValueProvider row, final PersistentPropertyAccessor accessor) { + protected void readPropertyFromRow(CassandraPersistentEntity entity, CassandraPersistentProperty property, + BasicCassandraRowValueProvider row, PersistentPropertyAccessor propertyAccessor) { // if true then skip; property was set in constructor if (entity.isConstructorArgument(property)) { @@ -179,7 +187,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter CassandraPersistentProperty keyProperty = entity.getIdProperty(); CassandraPersistentEntity keyEntity = keyProperty.getCompositePrimaryKeyEntity(); - Object key = accessor.getProperty(keyProperty); + Object key = propertyAccessor.getProperty(keyProperty); if (key == null) { key = instantiatePrimaryKey(keyEntity, keyProperty, row); @@ -189,7 +197,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter readPropertiesFromRow(property.getCompositePrimaryKeyEntity(), row, getConvertingAccessor(key, keyEntity)); // now that the key's properties have been populated, set the key property on the entity - accessor.setProperty(keyProperty, key); + propertyAccessor.setProperty(keyProperty, key); return; } @@ -198,17 +206,22 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return; } - accessor.setProperty(property, row.getPropertyValue(property)); + Object obj = getReadValue(property, row); + + propertyAccessor.setProperty(property, obj); } @SuppressWarnings("unused") protected Object instantiatePrimaryKey(CassandraPersistentEntity entity, CassandraPersistentProperty keyProperty, - BasicCassandraRowValueProvider propertyProvider) { + BasicCassandraRowValueProvider propertyProvider) { return instantiators.getInstantiatorFor(entity).createInstance(entity, new CassandraPersistentEntityParameterValueProvider(entity, propertyProvider, null)); } + /* (non-Javadoc) + * @see org.springframework.data.convert.EntityReader#read(java.lang.Class, S) + */ @Override public R read(Class type, Object row) { @@ -216,7 +229,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return readRow(type, (Row) row); } - throw new MappingException("Unknown row object " + row.getClass().getName()); + throw new MappingException("Unknown row object " + ObjectUtils.nullSafeClassName(row)); } @Override @@ -248,15 +261,14 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } protected void writeInsertFromWrapper(final ConvertingPropertyAccessor accessor, final Insert insert, - CassandraPersistentEntity entity) { + CassandraPersistentEntity entity) { entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(CassandraPersistentProperty property) { - Object value = accessor.getProperty(property, property.isCompositePrimaryKey() ? property.getType() - : CodecRegistry.DEFAULT_INSTANCE.codecFor(property.getDataType()).getJavaType().getRawType()); + Object value = getWriteValue(property, accessor); if (log.isDebugEnabled()) { log.debug("doWithProperties Property.type {}, Property.value {}", property.getType().getName(), value); @@ -268,7 +280,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } writeInsertFromWrapper(getConvertingAccessor(value, property.getCompositePrimaryKeyEntity()), insert, - property.getCompositePrimaryKeyEntity()); + property.getCompositePrimaryKeyEntity()); return; } @@ -287,15 +299,14 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } protected void writeUpdateFromWrapper(final ConvertingPropertyAccessor accessor, final Update update, - final CassandraPersistentEntity entity) { + final CassandraPersistentEntity entity) { entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(CassandraPersistentProperty property) { - Object value = accessor.getProperty(property, property.isCompositePrimaryKey() ? property.getType() - : CodecRegistry.DEFAULT_INSTANCE.codecFor(property.getDataType()).getJavaType().getRawType()); + Object value = getWriteValue(property, accessor); if (property.isCompositePrimaryKey()) { CassandraPersistentEntity keyEntity = property.getCompositePrimaryKeyEntity(); @@ -313,12 +324,12 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } protected void writeDeleteWhereFromObject(final Object object, final Where where, - CassandraPersistentEntity entity) { + CassandraPersistentEntity entity) { writeDeleteWhereFromWrapper(getConvertingAccessor(object, entity), where, entity); } protected void writeDeleteWhereFromWrapper(final ConvertingPropertyAccessor accessor, final Where where, - CassandraPersistentEntity entity) { + CassandraPersistentEntity entity) { // if the entity itself if a composite primary key, then we've recursed, so just add columns & return if (entity.isCompositePrimaryKey()) { @@ -326,19 +337,22 @@ public class MappingCassandraConverter extends AbstractCassandraConverter @Override public void doWithPersistentProperty(CassandraPersistentProperty prop) { - Object value = accessor.getProperty(prop, CodecRegistry.DEFAULT_INSTANCE.codecFor(prop.getDataType()).getJavaType().getRawType()); + Object value = accessor.getProperty(prop, + CodecRegistry.DEFAULT_INSTANCE.codecFor(prop.getDataType()).getJavaType().getRawType()); where.and(QueryBuilder.eq(prop.getColumnName().toCql(), value)); } }); + return; } // else, wrapper is an entity with an id Object id = getId(accessor, entity); + if (id == null) { - String msg = String.format("no id value found in object %s", accessor.getBean()); - log.error(msg); - throw new IllegalArgumentException(msg); + String message = String.format("no id value found in object %s", accessor.getBean()); + log.error(message); + throw new IllegalArgumentException(message); } if (id instanceof MapId) { @@ -358,7 +372,8 @@ public class MappingCassandraConverter extends AbstractCassandraConverter CassandraPersistentEntity idEntity = idProperty.getCompositePrimaryKeyEntity(); writeDeleteWhereFromWrapper(getConvertingAccessor(id, idEntity), where, - idProperty.getCompositePrimaryKeyEntity()); + idProperty.getCompositePrimaryKeyEntity()); + return; } @@ -372,12 +387,13 @@ public class MappingCassandraConverter extends AbstractCassandraConverter Assert.notNull(object); final ConvertingPropertyAccessor wrapper = getConvertingAccessor(object, entity); + object = wrapper.getBean(); if (!entity.getType().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( - String.format("given instance of type [%s] is not of compatible expected type [%s]", - object.getClass().getName(), entity.getType().getName())); + String.format("given instance of type [%s] is not of compatible expected type [%s]", + object.getClass().getName(), entity.getType().getName())); } if (object instanceof MapIdentifiable) { @@ -385,13 +401,15 @@ public class MappingCassandraConverter extends AbstractCassandraConverter } CassandraPersistentProperty idProperty = entity.getIdProperty(); + if (idProperty != null) { - return wrapper.getProperty(entity.getIdProperty(), - idProperty.isCompositePrimaryKey() ? idProperty.getType() : CodecRegistry.DEFAULT_INSTANCE.codecFor(idProperty.getDataType()).getJavaType().getRawType()); + return wrapper.getProperty(entity.getIdProperty(), idProperty.isCompositePrimaryKey() ? idProperty.getType() + : CodecRegistry.DEFAULT_INSTANCE.codecFor(idProperty.getDataType()).getJavaType().getRawType()); } // if the class doesn't have an id property, then it's using MapId final MapId id = id(); + entity.doWithProperties(new PropertyHandler() { @Override @@ -436,10 +454,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter */ private ConvertingPropertyAccessor getConvertingAccessor(Object source, CassandraPersistentEntity entity) { - PersistentPropertyAccessor accessor = (source instanceof PersistentPropertyAccessor - ? (PersistentPropertyAccessor) source : entity.getPropertyAccessor(source)); + PersistentPropertyAccessor propertyAccessor = (source instanceof PersistentPropertyAccessor + ? (PersistentPropertyAccessor) source : entity.getPropertyAccessor(source)); - return new ConvertingPropertyAccessor(accessor, conversionService); + return new ConvertingPropertyAccessor(propertyAccessor, conversionService); } /** @@ -451,4 +469,77 @@ public class MappingCassandraConverter extends AbstractCassandraConverter private boolean isPrimaryKeyPart(CassandraPersistentProperty property) { return (property.isCompositePrimaryKey() || property.isPrimaryKeyColumn() || property.isIdProperty()); } + + private Class getTargetType(CassandraPersistentProperty property) { + + return (property.isCompositePrimaryKey() ? property.getType() : CodecRegistry.DEFAULT_INSTANCE.codecFor( + mappingContext.getDataType(property)).getJavaType().getRawType()); + } + + /** + * Retrieve the value to write for the given {@link CassandraPersistentProperty} from + * {@link ConvertingPropertyAccessor} and perform optionally a conversion of collection element types. + * + * @param property the property. + * @param accessor the property accessor + * @return the return value, may be {@literal null}. + */ + @SuppressWarnings("unchecked") + private Object getWriteValue(CassandraPersistentProperty property, ConvertingPropertyAccessor accessor) { + + Object value = accessor.getProperty(property, getTargetType(property)); + + if (conversions.hasCustomWriteTarget(property.getActualType()) && property.isCollectionLike()) { + + Class customWriteTarget = conversions.getCustomWriteTarget(property.getActualType()); + if (Collection.class.isAssignableFrom(property.getType()) && value instanceof Collection) { + + Collection original = (Collection) value; + Collection converted = CollectionFactory.createCollection(property.getType(), original.size()); + + for (Object o : original) { + converted.add(getConversionService().convert(o, customWriteTarget)); + } + + value = converted; + } + } + + return value; + } + + /** + * Retrieve the value to read for the given {@link CassandraPersistentProperty} from + * {@link BasicCassandraRowValueProvider} and perform optionally a conversion of collection element types. + * + * @param property the property. + * @param row the row. + * @return the return value, may be {@literal null}. + */ + @SuppressWarnings("unchecked") + private Object getReadValue(CassandraPersistentProperty property, BasicCassandraRowValueProvider row) { + + Object obj = row.getPropertyValue(property); + + if (obj == null) { + return null; + } + + if (conversions.hasCustomWriteTarget(property.getActualType()) && property.isCollectionLike()) { + + if (Collection.class.isAssignableFrom(property.getType()) && obj instanceof Collection) { + + Collection original = (Collection) obj; + Collection converted = CollectionFactory.createCollection(property.getType(), original.size()); + + for (Object o : original) { + converted.add(getConversionService().convert(o, property.getActualType())); + } + + return converted; + } + } + + return obj; + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java index 24e61d99e..65e4ed1d8 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraMappingContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.data.cassandra.mapping; import static org.springframework.cassandra.core.cql.CqlIdentifier.*; import static org.springframework.cassandra.core.keyspace.CreateTableSpecification.*; +import static org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder.*; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; @@ -25,6 +26,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -33,6 +35,7 @@ import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.keyspace.CreateTableSpecification; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.context.MappingContext; @@ -43,6 +46,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import com.datastax.driver.core.DataType; import com.datastax.driver.core.TableMetadata; /** @@ -51,8 +55,10 @@ import com.datastax.driver.core.TableMetadata; * * @author Alex Shvid * @author Matthew T. Adams + * @author Mark Paluch */ -public class BasicCassandraMappingContext extends AbstractMappingContext, CassandraPersistentProperty> +public class BasicCassandraMappingContext + extends AbstractMappingContext, CassandraPersistentProperty> implements CassandraMappingContext, ApplicationContextAware { protected ApplicationContext context; @@ -70,13 +76,30 @@ public class BasicCassandraMappingContext extends AbstractMappingContext> nonPrimaryKeyEntities = new HashSet>(); protected Set> primaryKeyEntities = new HashSet>(); + private CustomConversions customConversions; + /** * Creates a new {@link BasicCassandraMappingContext}. */ public BasicCassandraMappingContext() { + + setCustomConversions(new CustomConversions(Collections.EMPTY_LIST)); setSimpleTypeHolder(CassandraSimpleTypeHolder.HOLDER); } + /** + * Sets the {@link CustomConversions}. + * + * @param customConversions must not be {@literal null}. + * @since 1.5 + */ + public void setCustomConversions(CustomConversions customConversions) { + + Assert.notNull(customConversions, "CustomConversions must not be null"); + + this.customConversions = customConversions; + } + @Override public void initialize() { super.initialize(); @@ -182,21 +205,20 @@ public class BasicCassandraMappingContext extends AbstractMappingContext type) { + + if (customConversions.hasCustomWriteTarget(type.getType())) { + return false; + } + + return super.shouldCreatePersistentEntityFor(type); + } + + /* (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#addPersistentEntity(org.springframework.data.util.TypeInformation) + */ + @Override + protected CassandraPersistentEntity addPersistentEntity(TypeInformation typeInformation) { + + // Prevent conversion types created as CassandraPersistentEntity + if (shouldCreatePersistentEntityFor(typeInformation)) { + return super.addPersistentEntity(typeInformation); + } + + return null; + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getDataType(org.springframework.data.cassandra.mapping.CassandraPersistentProperty) + */ + @Override + public DataType getDataType(CassandraPersistentProperty property) { + + if (property.isCompositePrimaryKey()) { + return property.getDataType(); + } + + if (property.findAnnotation(CassandraType.class) != null) { + return property.getDataType(); + } + + if (customConversions.hasCustomWriteTarget(property.getActualType())) { + + Class targetType = customConversions.getCustomWriteTarget(property.getActualType()); + + if (property.isCollectionLike()) { + + if (Set.class.isAssignableFrom(property.getType())) { + return DataType.set(getDataTypeFor(targetType)); + } + + if (List.class.isAssignableFrom(property.getType())) { + return DataType.list(getDataTypeFor(targetType)); + } + } + + return getDataTypeFor(targetType); + + } + + return property.getDataType(); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.mapping.CassandraMappingContext#getDataType(java.lang.Class) + */ + @Override + public DataType getDataType(Class type) { + + if (customConversions.hasCustomWriteTarget(type)) { + return getDataTypeFor(customConversions.getCustomWriteTarget(type)); + } + + return getDataTypeFor(type); + } + public void setMapping(Mapping mapping) { Assert.notNull(mapping); @@ -236,15 +334,13 @@ public class BasicCassandraMappingContext extends AbstractMappingContext entity = getPersistentEntity(entityClass); if (entity == null) { - throw new IllegalStateException(String.format("unknown persistent entity class name [%s]", - entityClassName)); + throw new IllegalStateException(String.format("unknown persistent entity class name [%s]", entityClassName)); } String tableName = entityMapping.getTableName(); @@ -270,7 +366,7 @@ public class BasicCassandraMappingContext extends AbstractMappingContext(columnNames)); + this.columnNames = this.explicitColumnNames = Collections + .unmodifiableList(new ArrayList(columnNames)); } @Override @@ -368,8 +375,8 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP this.forceQuote = forceQuote; } - List columnNames = new ArrayList(this.columnNames == null ? 0 - : this.columnNames.size()); + List columnNames = new ArrayList( + this.columnNames == null ? 0 : this.columnNames.size()); for (CqlIdentifier columnName : getColumnNames()) { columnNames.add(cqlId(columnName.getUnquoted(), forceQuote)); @@ -382,8 +389,8 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP public List getCompositePrimaryKeyProperties() { if (!isCompositePrimaryKey()) { - throw new IllegalStateException(String.format( - "[%s] does not represent a composite primary key property", getName())); + throw new IllegalStateException( + String.format("[%s] does not represent a composite primary key property", getName())); } return getCompositePrimaryKeyEntity().getCompositePrimaryKeyProperties(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java index 1f3f3459c..8e6404474 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraMappingContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,17 +18,20 @@ package org.springframework.data.cassandra.mapping; import java.util.Collection; import org.springframework.cassandra.core.keyspace.CreateTableSpecification; +import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.mapping.context.MappingContext; +import com.datastax.driver.core.DataType; import com.datastax.driver.core.TableMetadata; /** * A {@link MappingContext} for Cassandra. * * @author Matthew T. Adams + * @author Mark Paluch */ -public interface CassandraMappingContext extends - MappingContext, CassandraPersistentProperty> { +public interface CassandraMappingContext + extends MappingContext, CassandraPersistentProperty> { /** * Returns only those entities that don't represent primary key types. @@ -36,7 +39,7 @@ public interface CassandraMappingContext extends * @see #getPersistentEntities(boolean) */ @Override - public Collection> getPersistentEntities(); + Collection> getPersistentEntities(); /** * Returns all persistent entities or only non-primary-key entities. @@ -44,7 +47,7 @@ public interface CassandraMappingContext extends * @param includePrimaryKeyTypes If true, returns all entities, including entities that represent primary * key types. If false, returns only entities that don't represent primary key types. */ - public Collection> getPersistentEntities(boolean includePrimaryKeyTypes); + Collection> getPersistentEntities(boolean includePrimaryKeyTypes); /** * Returns only those entities representing primary key types. @@ -91,4 +94,28 @@ public interface CassandraMappingContext extends * Sets a verifier other than the {@link BasicCassandraPersistentEntityMetadataVerifier} */ void setVerifier(CassandraPersistentEntityMetadataVerifier verifier); + + /** + * Retrieve the data type of the property. Cassandra {@link DataType types} are determined using simple types and + * configured {@link CustomConversions}. + * + * @param property must not be {@literal null}. + * @return the Cassandra {@link DataType type}. + * @see CustomConversions + * @see CassandraSimpleTypeHolder + * @since 1.5 + */ + DataType getDataType(CassandraPersistentProperty property); + + /** + * Retrieve the data type based on the given {@code type}. Cassandra {@link DataType types} are determined using simple types and + * configured {@link CustomConversions}. + * + * @param type must not be {@literal null}. + * @return the Cassandra {@link DataType type}. + * @see CustomConversions + * @see CassandraSimpleTypeHolder + * @since 1.5 + */ + DataType getDataType(Class type); } 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 7a94d654e..6ac2d502b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ 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.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.util.TypeInformation; @@ -32,9 +33,10 @@ import com.datastax.driver.core.DataType; * @author Alex Shvid * @author Matthew T. Adams * @author David T. Webb + * @author Mark Paluch */ -public interface CassandraPersistentProperty extends PersistentProperty, - ApplicationContextAware { +public interface CassandraPersistentProperty + extends PersistentProperty, ApplicationContextAware { /** * Whether the property is a composite primary key. @@ -78,7 +80,11 @@ public interface CassandraPersistentProperty extends PersistentProperty> simpleTypes = getCassandraPrimitiveTypes(codecRegistry); simpleTypes.add(Number.class); + simpleTypes.add(Row.class); classToDataType = Collections.unmodifiableMap(classToDataType(primitiveWrappers, codecRegistry)); nameToDataType = Collections.unmodifiableMap(nameToDataType()); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraType.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraType.java index 9f8a88528..a873829cd 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraType.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraType.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors + * Copyright 2013-2016 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,11 @@ import java.lang.annotation.RetentionPolicy; import com.datastax.driver.core.DataType; /** - * Specifies the Cassandra type of the annotated property. + * Specifies the Cassandra type of the annotated property or parameter if used in query methods. * * @author Alex Shvid * @author Matthew T. Adams + * @author Mark Paluch */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java index 57d769eb2..5bcb81e85 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java @@ -80,7 +80,7 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { @Override public Object execute(Object[] parameters) { - CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(method, parameters); + CassandraParameterAccessor accessor = new ConvertingParameterAccessor(template.getConverter(), new CassandraParametersParameterAccessor(method, parameters)); String query = createQuery(accessor); ResultProcessor processor = method.getResultProcessor().withDynamicProjection(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 index b8d0b69a4..1575e2858 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java @@ -1,5 +1,51 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.cassandra.repository.query; import org.springframework.data.repository.query.ParameterAccessor; -public interface CassandraParameterAccessor extends ParameterAccessor {} +import com.datastax.driver.core.DataType; + +/** + * Cassandra-specific {@link ParameterAccessor} exposing a Cassandra {@link DataType types} that are supported by the + * driver and parameter type. + * + * @author Matthew Adams + * @author Mark Paluch + */ +public interface CassandraParameterAccessor extends ParameterAccessor { + + /** + * Returns the Cassandra {@link DataType} for the declared parameter if the type is a + * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder simple type}. Parameter types may be + * specified using {@link org.springframework.data.cassandra.mapping.CassandraType}. + * + * @param index the parameter index + * @return the Cassandra {@link DataType} or {@literal null} if the parameter type cannot be determined from + * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder} + * @see org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder + * @see org.springframework.data.cassandra.mapping.CassandraType + */ + DataType getDataType(int index); + + /** + * The actual parameter type (after unwrapping). + * + * @param index the parameter index + * @return the parameter type, never {@literal null}. + */ + Class getParameterType(int index); +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java index aba240973..e08dfedb1 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameters.java @@ -1,37 +1,108 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.cassandra.repository.query; import java.lang.reflect.Method; import java.util.List; import org.springframework.core.MethodParameter; +import org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder; +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.cassandra.repository.query.CassandraParameters.CassandraParameter; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; -public class CassandraParameters extends Parameters { +import com.datastax.driver.core.DataType; - public CassandraParameters(List originals) { - super(originals); - } +/** + * Custom extension of {@link Parameters} discovering additional properties of query method parameters. + * + * @author Matthew Adams + * @author Mark Paluch + */ +public class CassandraParameters extends Parameters { + /** + * Creates a new {@link CassandraParameters} instance from the given {@link Method} + * + * @param method must not be {@literal null}. + */ public CassandraParameters(Method method) { super(method); } + private CassandraParameters(List originals) { + super(originals); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.Parameters#createParameter(org.springframework.core.MethodParameter) + */ @Override protected CassandraParameter createParameter(MethodParameter parameter) { return new CassandraParameter(parameter); } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List) + */ @Override - protected CassandraParameters createFrom(List parameters) { + protected CassandraParameters createFrom(List parameters) { return new CassandraParameters(parameters); } + /** + * Custom {@link Parameter} implementation adding {@link CassandraType} support. + * + * @author Mark Paluch + */ class CassandraParameter extends Parameter { + private final DataType dataType; + protected CassandraParameter(MethodParameter parameter) { + super(parameter); + + if (parameter.hasParameterAnnotation(CassandraType.class)) { + + CassandraType cassandraType = parameter.getParameterAnnotation(CassandraType.class); + + if (cassandraType.type() == null) { + throw new IllegalArgumentException( + String.format("You must specify the type() when annotating method parameters with @%s", + CassandraType.class.getSimpleName())); + } + + this.dataType = CassandraSimpleTypeHolder.getDataTypeFor(cassandraType.type()); + } else { + this.dataType = CassandraSimpleTypeHolder.getDataTypeFor(getType()); + } } + /** + * Returns the Cassandra {@link DataType} for the declared parameter if the type is a + * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder simple type}. Parameter types may be + * specified using {@link org.springframework.data.cassandra.mapping.CassandraType}. + * + * @return the Cassandra {@link DataType} or {@literal null} if the parameter type cannot be determined from + * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder} + */ + public DataType getCassandraType() { + return dataType; + } } } 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 index f460c31d7..f75f2812d 100644 --- 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 @@ -1,17 +1,71 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.cassandra.repository.query; +import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; -public class CassandraParametersParameterAccessor extends ParametersParameterAccessor implements - CassandraParameterAccessor { +import com.datastax.driver.core.DataType; + +/** + * Cassandra-specific {@link ParameterAccessor} exposing a Cassandra {@link DataType types} that are supported by the + * driver and parameter type. + * + * @author Mark Paluch + */ +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}. + * @param values must not be {@literal null}. */ public CassandraParametersParameterAccessor(CassandraQueryMethod method, Object... values) { super(method.getParameters(), values); } + + /** + * Returns the Cassandra {@link DataType} for the declared parameter if the type is a + * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder simple type}. Parameter types may be + * specified using {@link org.springframework.data.cassandra.mapping.CassandraType}. + * + * @param index parameter index + * @return the Cassandra {@link DataType} or {@literal null} if the parameter type cannot be determined from + * {@link org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder} + */ + public DataType getDataType(int index) { + return getParameters().getParameter(index).getCassandraType(); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.CassandraParameterAccessor#getParameterType(int) + */ + @Override + public Class getParameterType(int index) { + return getParameters().getParameter(index).getType(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.ParametersParameterAccessor#getParameters() + */ + @Override + public CassandraParameters getParameters() { + return (CassandraParameters) super.getParameters(); + } + } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java index 94547c666..f9a79295a 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java @@ -1,18 +1,23 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.cassandra.repository.query; import java.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.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.mapping.CassandraMappingContext; @@ -23,54 +28,43 @@ 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; +/** + * Cassandra specific implementation of {@link QueryMethod}. + * + * @author Matthew Adams + * @author Oliver Gierke + * @author Mark Paluch + */ 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, int.class, Integer.class })); + private final Method method; + private final CassandraMappingContext mappingContext; + private Query query; + private String queryString; + private boolean queryCached = false; - public static final List> STRING_LIKE_PARAMETER_TYPES = Collections.unmodifiableList(Arrays - .asList(new Class[] { CharSequence.class, char.class, Character.class, char[].class })); - - public static final List> DATE_PARAMETER_TYPES = Collections.unmodifiableList(Arrays - .asList(new Class[] { Date.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; - protected Set stringLikeParameterIndexes = new HashSet(); - protected Set dateParameterIndexes = new HashSet(); - - public CassandraQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, CassandraMappingContext mappingContext) { + /** + * Creates a new {@link CassandraQueryMethod} from the given {@link Method}. + * + * @param method must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @param projectionFactory must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public CassandraQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, + CassandraMappingContext mappingContext) { super(method, metadata, factory); + Assert.notNull(mappingContext, "MappingContext must not be null!"); + verify(method, metadata); this.method = method; - - Assert.notNull(mappingContext, "MappingContext must not be null!"); this.mappingContext = mappingContext; } @@ -78,36 +72,13 @@ public class CassandraQueryMethod extends QueryMethod { // TODO: support Page & Slice queries if (isSliceQuery() || isPageQuery()) { - throw new InvalidDataAccessApiUsageException("neither slice nor page queries are supported yet"); - } - - Set> offendingTypes = new HashSet>(); - - int i = 0; - for (Class type : method.getParameterTypes()) { - if (!ALLOWED_PARAMETER_TYPES.contains(type)) { - offendingTypes.add(type); - } - for (Class quotedType : STRING_LIKE_PARAMETER_TYPES) { - if (quotedType.isAssignableFrom(type)) { - stringLikeParameterIndexes.add(i); - } - } - for (Class quotedType : DATE_PARAMETER_TYPES) { - if (quotedType.isAssignableFrom(type)) { - dateParameterIndexes.add(i); - } - } - i++; - } - - 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)); + throw new InvalidDataAccessApiUsageException("Slice and Page queries are not supported."); } } + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method) + */ @Override protected CassandraParameters createParameters(Method method) { return new CassandraParameters(method); @@ -118,7 +89,7 @@ public class CassandraQueryMethod extends QueryMethod { */ Query getQueryAnnotation() { if (query == null) { - query = method.getAnnotation(Query.class); + query = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); queryCached = true; } return query; @@ -145,42 +116,17 @@ public class CassandraQueryMethod extends QueryMethod { return queryString; } + /** + * @return the return type for this {@link QueryMethod}. + */ public TypeInformation getReturnType() { return ClassTypeInformation.fromReturnTypeOf(method); } + /** + * @return true is the method returns a {@link ResultSet}. + */ 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()); - } - - public boolean isStringLikeParameter(int parameterIndex) { - return stringLikeParameterIndexes.contains(parameterIndex); - } - - public boolean isDateParameter(int parameterIndex) { - return dateParameterIndexes.contains(parameterIndex); + return ResultSet.class.isAssignableFrom(getReturnType().getActualType().getType()); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java new file mode 100644 index 000000000..ef59804f5 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java @@ -0,0 +1,178 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import java.util.Iterator; + +import org.springframework.data.cassandra.convert.CassandraConverter; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.TypeCodec; + +/** + * Custom {@link org.springframework.data.repository.query.ParameterAccessor} that uses a {@link CassandraConverter} to + * convert parameters. + * + * @author Mark Paluch + * @since 1.5 + */ +class ConvertingParameterAccessor implements CassandraParameterAccessor { + + private final CassandraConverter cassandraConverter; + private final CassandraParameterAccessor delegate; + + public ConvertingParameterAccessor(CassandraConverter cassandraConverter, CassandraParameterAccessor delegate) { + + this.cassandraConverter = cassandraConverter; + this.delegate = delegate; + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.ParameterAccessor#getPageable() + */ + @Override + public Pageable getPageable() { + return delegate.getPageable(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.ParameterAccessor#getSort() + */ + @Override + public Sort getSort() { + return delegate.getSort(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.ParameterAccessor#getDynamicProjection() + */ + @Override + public Class getDynamicProjection() { + return delegate.getDynamicProjection(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.ParameterAccessor#getBindableValue(int) + */ + @Override + public Object getBindableValue(int index) { + return potentiallyConvert(index, delegate.getBindableValue(index)); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.CassandraParameterAccessor#getDataType(int) + */ + @Override + public DataType getDataType(int index) { + return delegate.getDataType(index); + } + + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.CassandraParameterAccessor#getParameterType(int) + */ + @Override + public Class getParameterType(int index) { + return delegate.getParameterType(index); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.ParameterAccessor#hasBindableNullValue() + */ + @Override + public boolean hasBindableNullValue() { + return delegate.hasBindableNullValue(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Iterable#iterator() + */ + /* (non-Javadoc) + * @see org.springframework.data.repository.query.ParameterAccessor#iterator() + */ + public Iterator iterator() { + return new ConvertingIterator(delegate.iterator()); + } + + private Object potentiallyConvert(int index, Object bindableValue) { + + if (bindableValue == null) { + return null; + } + + DataType parameterType = getDataType(index); + if (parameterType == null) { + parameterType = cassandraConverter.getMappingContext().getDataType(getParameterType(index)); + } + + TypeCodec cassandraType = CodecRegistry.DEFAULT_INSTANCE.codecFor(parameterType); + + if (cassandraType.getJavaType().getRawType().isAssignableFrom(bindableValue.getClass())) { + return bindableValue; + } + + return cassandraConverter.getConversionService().convert(bindableValue, cassandraType.getJavaType().getRawType()); + } + + /** + * Custom {@link Iterator} to convert items before returning them. + * + * @author Mark Paluch + */ + private class ConvertingIterator implements Iterator { + + private final Iterator delegate; + private int index = 0; + + /** + * Creates a new {@link ConvertingIterator} for the given delegate. + * + * @param delegate + */ + public ConvertingIterator(Iterator delegate) { + this.delegate = delegate; + } + + /* + * (non-Javadoc) + * @see java.util.Iterator#hasNext() + */ + public boolean hasNext() { + return delegate.hasNext(); + } + + /* + * (non-Javadoc) + * @see java.util.Iterator#next() + */ + public Object next() { + return potentiallyConvert(index++, next()); + } + + /* + * (non-Javadoc) + * @see java.util.Iterator#remove() + */ + public void remove() { + delegate.remove(); + } + } + +} 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 index a5f4d3e81..68d1a6adb 100644 --- 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 @@ -1,20 +1,47 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.cassandra.repository.query; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.cassandra.core.cql.CqlStringUtils; import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.util.ClassUtils; +import com.datastax.driver.core.LocalDate; + +/** + * Query to use a plain String to create the {@link Query} to actually execute. + * + * @author Matthew Adams + * @author Mark Paluch + */ public class StringBasedCassandraQuery extends AbstractCassandraQuery { - private static final Pattern PLACEHOLDER = Pattern.compile("\\?(\\d+)"); - private static final Logger LOG = LoggerFactory.getLogger(StringBasedCassandraQuery.class); + @SuppressWarnings("unchecked") private static final Set> STRING_LIKE_PARAMETER_TYPES = new HashSet>( + Arrays.asList(CharSequence.class, char.class, Character.class, char[].class)); - protected String query; + private static final Pattern PLACEHOLDER = Pattern.compile("\\?(\\d+)"); + + protected final String query; public StringBasedCassandraQuery(String query, CassandraQueryMethod queryMethod, CassandraOperations operations) { @@ -27,6 +54,9 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { this(queryMethod.getAnnotatedQuery(), queryMethod, operations); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.AbstractCassandraQuery#createQuery(org.springframework.data.cassandra.repository.query.CassandraParameterAccessor) + */ @Override public String createQuery(CassandraParameterAccessor accessor) { return replacePlaceholders(query, accessor); @@ -41,13 +71,14 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { String group = matcher.group(); int index = Integer.parseInt(matcher.group(1)); Object value = getParameterWithIndex(accessor, index); - String stringValue = null; - CassandraQueryMethod queryMethod = getQueryMethod(); + String stringValue; - if (queryMethod.isStringLikeParameter(index)) { - stringValue = "'" + CqlStringUtils.escapeSingle(value) + "'"; - } else if (queryMethod.isDateParameter(index)) { - stringValue = "" + ((Date) value).getTime(); + if (isStringLike(value)) { + stringValue = String.format("'%s'", CqlStringUtils.escapeSingle(value)); + } else if (isTimestampParameter(value)) { + stringValue = String.format("%d", ((Date) value).getTime()); + } else if (isDateParameter(value)) { + stringValue = String.format("'%s'", value); } else { stringValue = value.toString(); } @@ -58,6 +89,30 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { return result; } + private boolean isTimestampParameter(Object value) { + return value instanceof Date; + } + + private boolean isDateParameter(Object value) { + return value instanceof LocalDate; + } + + private boolean isStringLike(Object value) { + + if (value == null) { + return false; + } + + for (Class type : STRING_LIKE_PARAMETER_TYPES) { + + if (ClassUtils.isAssignableValue(type, value)) { + return true; + } + } + + return false; + } + private Object getParameterWithIndex(CassandraParameterAccessor accessor, int index) { return accessor.getBindableValue(index); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/RowMockUtil.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/RowMockUtil.java new file mode 100644 index 000000000..24f6fd178 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/RowMockUtil.java @@ -0,0 +1,156 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.util.Assert; + +import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.Row; + +/** + * Utility to mock a Cassandra {@link Row}. + * + * @author Mark Paluch + */ +public class RowMockUtil { + + /** + * Creates a new {@link Row} mock using the given {@code columns}. Each column carries a name, value and data type so + * users of {@link Row} can use most of the methods. + * + * @param columns + * @return + */ + public static Row newRowMock(final Column... columns) { + + Assert.notNull(columns, "Columns must not be null"); + + Row rowMock = mock(Row.class); + ColumnDefinitions columnDefinitionsMock = mock(ColumnDefinitions.class); + + when(rowMock.getColumnDefinitions()).thenReturn(columnDefinitionsMock); + + when(columnDefinitionsMock.contains(anyString())).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + + for (Column column : columns) { + if (column.name.equalsIgnoreCase((String) invocation.getArguments()[0])) { + return true; + } + } + + return false; + } + }); + + when(columnDefinitionsMock.getIndexOf(anyString())).thenAnswer(new Answer() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + + int counter = 0; + for (Column column : columns) { + if (column.name.equalsIgnoreCase((String) invocation.getArguments()[0])) { + return counter; + } + counter++; + } + + return -1; + } + }); + + when(columnDefinitionsMock.getType(anyString())).thenAnswer(new Answer() { + @Override + public DataType answer(InvocationOnMock invocation) throws Throwable { + + for (Column column : columns) { + if (column.name.equalsIgnoreCase((String) invocation.getArguments()[0])) { + return column.type; + } + } + + return null; + } + }); + + when(columnDefinitionsMock.getType(anyInt())).thenAnswer(new Answer() { + @Override + public DataType answer(InvocationOnMock invocation) throws Throwable { + return columns[(Integer) invocation.getArguments()[0]].type; + } + }); + + when(rowMock.getObject(anyInt())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return columns[(Integer) invocation.getArguments()[0]].value; + } + }); + + when(rowMock.getObject(anyString())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + + for (Column column : columns) { + if (column.name.equalsIgnoreCase((String) invocation.getArguments()[0])) { + return column.value; + } + } + + return null; + } + }); + + return rowMock; + } + + /** + * Creates a new {@link Column} to be used with {@link RowMockUtil#newRowMock(Column...)}. + * + * @param name must not be empty or {@link null}. + * @param value can be {@literal null}. + * @param type must not be {@literal null}. + * @return + */ + public static Column column(String name, Object value, DataType type) { + + Assert.hasText(name, "Name must not be empty"); + Assert.notNull(type, "DataType must not be null"); + + return new Column(name, value, type); + } + + public static class Column { + + private final String name; + private final Object value; + private final DataType type; + + Column(String name, Object value, DataType type) { + this.name = name; + this.value = value; + this.type = type; + } + } + +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/ConverterRegistrationUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/ConverterRegistrationUnitTests.java index c03cfaa06..159a762cb 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/ConverterRegistrationUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/ConverterRegistrationUnitTests.java @@ -20,6 +20,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import org.junit.Test; +import org.springframework.data.cassandra.domain.Person; /** * Unit tests for {@link ConverterRegistration}. @@ -72,8 +73,4 @@ public class ConverterRegistrationUnitTests { assertThat(context.isWriting(), is(true)); assertThat(context.isReading(), is(true)); } - - public static class Person { - - } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CustomConversionsUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CustomConversionsUnitTests.java index aafe1fceb..082b696b8 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CustomConversionsUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CustomConversionsUnitTests.java @@ -37,6 +37,9 @@ import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.convert.WritingConverter; +import org.threeten.bp.LocalDateTime; + +import com.datastax.driver.core.Row; /** * Unit tests for {@link CustomConversions}. @@ -140,6 +143,16 @@ public class CustomConversionsUnitTests { assertThat(conversions.isSimpleType(InetAddress.class), is(true)); } + /** + * @see DATACASS-280 + */ + @Test + public void considersRowASimpleType() { + + CustomConversions conversions = new CustomConversions(); + assertThat(conversions.isSimpleType(Row.class), is(true)); + } + /** * @see DATACASS-280 */ @@ -197,6 +210,39 @@ public class CustomConversionsUnitTests { assertThat(customConversions.getCustomWriteTarget(String.class, SimpleDateFormat.class), notNullValue()); } + /** + * @see DATACASS-296 + */ + @Test + public void registersConvertersForJsr310() { + + CustomConversions customConversions = new CustomConversions(); + + assertThat(customConversions.hasCustomWriteTarget(java.time.LocalDateTime.class), is(true)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void registersConvertersForThreeTenBackPort() { + + CustomConversions customConversions = new CustomConversions(); + + assertThat(customConversions.hasCustomWriteTarget(org.threeten.bp.LocalDateTime.class), is(true)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void registersConvertersForJoda() { + + CustomConversions customConversions = new CustomConversions(); + + assertThat(customConversions.hasCustomWriteTarget(org.joda.time.LocalDate.class), is(true)); + } + private static Class createProxyTypeFor(Class type) { ProxyFactory factory = new ProxyFactory(); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUnitTests.java index 28cbbc0ee..1933c90d1 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/MappingCassandraConverterUnitTests.java @@ -22,15 +22,23 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assume.*; import static org.mockito.Mockito.*; +import static org.springframework.data.cassandra.RowMockUtil.*; import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.UUID; import org.junit.Before; @@ -43,6 +51,7 @@ import org.mockito.runners.MockitoJUnitRunner; import org.springframework.cassandra.core.PrimaryKeyType; import org.springframework.core.SpringVersion; import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.data.cassandra.RowMockUtil; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraType; @@ -53,6 +62,7 @@ import org.springframework.data.cassandra.mapping.Table; import org.springframework.test.util.ReflectionTestUtils; import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.DataType; import com.datastax.driver.core.DataType.Name; import com.datastax.driver.core.LocalDate; import com.datastax.driver.core.Row; @@ -71,6 +81,7 @@ import com.datastax.driver.core.querybuilder.Update.Assignments; * @author Mark Paluch * @soundtrack Outlandich - Dont Leave Me Feat Cyt (Sun Kidz Electrocore Mix) */ +@SuppressWarnings("Since15") @RunWith(MockitoJUnitRunner.class) public class MappingCassandraConverterUnitTests { @@ -88,6 +99,7 @@ public class MappingCassandraConverterUnitTests { mappingCassandraConverter = new MappingCassandraConverter(mappingContext); mappingCassandraConverter.afterPropertiesSet(); + } /** @@ -373,7 +385,7 @@ public class MappingCassandraConverterUnitTests { * @see DATACASS-280 */ @Test - public void shouldReadInetAddressCorrectly() throws UnknownHostException { + public void shouldReadInetAddressCorrectly() throws UnknownHostException{ InetAddress localHost = InetAddress.getLocalHost(); when(rowMock.getInet(0)).thenReturn(localHost); @@ -388,7 +400,7 @@ public class MappingCassandraConverterUnitTests { * @see DATACASS-271 */ @Test - public void shouldReadTimestampCorrectly() throws UnknownHostException { + public void shouldReadTimestampCorrectly() { Date date = new Date(1); when(rowMock.getTimestamp(0)).thenReturn(date); @@ -402,7 +414,7 @@ public class MappingCassandraConverterUnitTests { * @see DATACASS-271 */ @Test - public void shouldReadDateCorrectly() throws UnknownHostException { + public void shouldReadDateCorrectly() { LocalDate date = LocalDate.fromDaysSinceEpoch(1234); when(rowMock.getDate(0)).thenReturn(date); @@ -416,7 +428,7 @@ public class MappingCassandraConverterUnitTests { * @see DATACASS-280 */ @Test - public void shouldReadBooleanCorrectly() throws UnknownHostException { + public void shouldReadBooleanCorrectly() { when(rowMock.getBool(0)).thenReturn(true); @@ -425,6 +437,307 @@ public class MappingCassandraConverterUnitTests { assertThat(result, is(equalTo(true))); } + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadLocalDateCorrectly() { + + LocalDateTime now = LocalDateTime.now(); + Instant instant = now.toInstant(ZoneOffset.UTC); + + Row rowMock = RowMockUtil.newRowMock(column("id", "my-id", DataType.ascii()), + column("localdate", Date.from(instant), DataType.timestamp())); + + TypeWithLocalDate result = mappingCassandraConverter.readRow(TypeWithLocalDate.class, rowMock); + + assertThat(result.localDate, is(notNullValue())); + assertThat(result.localDate.getYear(), is(now.getYear())); + assertThat(result.localDate.getMonthValue(), is(now.getMonthValue())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateInsertWithLocalDateCorrectly() { + + java.time.LocalDate now = java.time.LocalDate.now(); + + TypeWithLocalDate typeWithLocalDate = new TypeWithLocalDate(); + typeWithLocalDate.localDate = now; + + Insert insert = QueryBuilder.insertInto("table"); + mappingCassandraConverter.write(typeWithLocalDate, insert); + + assertThat(getValues(insert), + contains((Object) LocalDate.fromYearMonthDay(now.getYear(), now.getMonthValue(), now.getDayOfMonth()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateUpdateWithLocalDateCorrectly() { + + java.time.LocalDate now = java.time.LocalDate.now(); + + TypeWithLocalDate typeWithLocalDate = new TypeWithLocalDate(); + typeWithLocalDate.localDate = now; + + Update update = QueryBuilder.update("table"); + mappingCassandraConverter.write(typeWithLocalDate, update); + + assertThat(getAssignmentValues(update), + contains((Object) LocalDate.fromYearMonthDay(now.getYear(), now.getMonthValue(), now.getDayOfMonth()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateInsertWithLocalDateListUsingCassandraDate() { + + java.time.LocalDate now = java.time.LocalDate.now(); + + TypeWithLocalDate typeWithLocalDate = new TypeWithLocalDate(); + java.time.LocalDate localDate = java.time.LocalDate.of(2010, 7, 4); + typeWithLocalDate.list = Arrays.asList(now, localDate); + + Insert insert = QueryBuilder.insertInto("table"); + mappingCassandraConverter.write(typeWithLocalDate, insert); + + List values = getValues(insert); + + assertThat(values.get(0), is(instanceOf(List.class))); + List dates = (List) values.get(0); + + assertThat(dates, hasItem(LocalDate.fromYearMonthDay(now.getYear(), now.getMonthValue(), now.getDayOfMonth()))); + assertThat(dates, hasItem(LocalDate.fromYearMonthDay(2010, 7, 4))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateInsertWithLocalDateSetUsingCassandraDate() { + + java.time.LocalDate now = java.time.LocalDate.now(); + + TypeWithLocalDate typeWithLocalDate = new TypeWithLocalDate(); + java.time.LocalDate localDate = java.time.LocalDate.of(2010, 7, 4); + typeWithLocalDate.set = new HashSet(Arrays.asList(now, localDate)); + + Insert insert = QueryBuilder.insertInto("table"); + mappingCassandraConverter.write(typeWithLocalDate, insert); + + List values = getValues(insert); + + assertThat(values.get(0), is(instanceOf(Set.class))); + Set dates = (Set) values.get(0); + + assertThat(dates, hasItem(LocalDate.fromYearMonthDay(now.getYear(), now.getMonthValue(), now.getDayOfMonth()))); + assertThat(dates, hasItem(LocalDate.fromYearMonthDay(2010, 7, 4))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadLocalDateTimeUsingCassandraDateCorrectly() { + + Row rowMock = RowMockUtil.newRowMock(column("id", "my-id", DataType.ascii()), + column("localDate", LocalDate.fromYearMonthDay(2010, 7, 4), DataType.date())); + + TypeWithLocalDateMappedToDate result = mappingCassandraConverter.readRow(TypeWithLocalDateMappedToDate.class, + rowMock); + + assertThat(result.localDate, is(notNullValue())); + assertThat(result.localDate.getYear(), is(2010)); + assertThat(result.localDate.getMonthValue(), is(7)); + assertThat(result.localDate.getDayOfMonth(), is(4)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateInsertWithLocalDateUsingCassandraDateCorrectly() { + + TypeWithLocalDateMappedToDate typeWithLocalDate = new TypeWithLocalDateMappedToDate(); + typeWithLocalDate.localDate = java.time.LocalDate.of(2010, 7, 4); + + Insert insert = QueryBuilder.insertInto("table"); + + mappingCassandraConverter.write(typeWithLocalDate, insert); + + assertThat(getValues(insert), contains((Object) LocalDate.fromYearMonthDay(2010, 7, 4))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateUpdateWithLocalDateUsingCassandraDateCorrectly() { + + TypeWithLocalDateMappedToDate typeWithLocalDate = new TypeWithLocalDateMappedToDate(); + typeWithLocalDate.localDate = java.time.LocalDate.of(2010, 7, 4); + + Update update = QueryBuilder.update("table"); + mappingCassandraConverter.write(typeWithLocalDate, update); + + assertThat(getAssignmentValues(update), contains((Object) LocalDate.fromYearMonthDay(2010, 7, 4))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadLocalDateTimeCorrectly() { + + LocalDateTime now = LocalDateTime.now(); + Instant instant = now.toInstant(ZoneOffset.UTC); + + Row rowMock = RowMockUtil.newRowMock(column("id", "my-id", DataType.ascii()), + column("localDateTime", Date.from(instant), DataType.timestamp())); + + TypeWithLocalDate result = mappingCassandraConverter.readRow(TypeWithLocalDate.class, rowMock); + + assertThat(result.localDateTime, is(notNullValue())); + assertThat(result.localDateTime.getYear(), is(now.getYear())); + assertThat(result.localDateTime.getMinute(), is(now.getMinute())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadInstantCorrectly() { + + LocalDateTime now = LocalDateTime.now(); + Instant instant = now.toInstant(ZoneOffset.UTC); + + Row rowMock = RowMockUtil.newRowMock(column("id", "my-id", DataType.ascii()), + column("instant", Date.from(instant), DataType.timestamp())); + + TypeWithInstant result = mappingCassandraConverter.readRow(TypeWithInstant.class, rowMock); + + assertThat(result.instant, is(notNullValue())); + assertThat(result.instant.getEpochSecond(), is(instant.getEpochSecond())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadZoneIdCorrectly() { + + Row rowMock = RowMockUtil.newRowMock(column("id", "my-id", DataType.ascii()), + column("zoneId", "Europe/Paris", DataType.varchar())); + + TypeWithZoneId result = mappingCassandraConverter.readRow(TypeWithZoneId.class, rowMock); + + assertThat(result.zoneId, is(notNullValue())); + assertThat(result.zoneId.getId(), is(equalTo("Europe/Paris"))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadJodaLocalDateTimeUsingCassandraDateCorrectly() { + + Row rowMock = RowMockUtil.newRowMock(column("id", "my-id", DataType.ascii()), + column("localDate", LocalDate.fromYearMonthDay(2010, 7, 4), DataType.date())); + + TypeWithJodaLocalDateMappedToDate result = mappingCassandraConverter + .readRow(TypeWithJodaLocalDateMappedToDate.class, rowMock); + + assertThat(result.localDate, is(notNullValue())); + assertThat(result.localDate.getYear(), is(2010)); + assertThat(result.localDate.getMonthOfYear(), is(7)); + assertThat(result.localDate.getDayOfMonth(), is(4)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateInsertWithJodaLocalDateUsingCassandraDateCorrectly() { + + TypeWithJodaLocalDateMappedToDate typeWithLocalDate = new TypeWithJodaLocalDateMappedToDate(); + typeWithLocalDate.localDate = new org.joda.time.LocalDate(2010, 7, 4); + + Insert insert = QueryBuilder.insertInto("table"); + mappingCassandraConverter.write(typeWithLocalDate, insert); + + assertThat(getValues(insert), contains((Object) LocalDate.fromYearMonthDay(2010, 7, 4))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateUpdateWithJodaLocalDateUsingCassandraDateCorrectly() { + + TypeWithJodaLocalDateMappedToDate typeWithLocalDate = new TypeWithJodaLocalDateMappedToDate(); + typeWithLocalDate.localDate = new org.joda.time.LocalDate(2010, 7, 4); + + Update update = QueryBuilder.update("table"); + mappingCassandraConverter.write(typeWithLocalDate, update); + + assertThat(getAssignmentValues(update), contains((Object) LocalDate.fromYearMonthDay(2010, 7, 4))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadThreeTenBpLocalDateTimeUsingCassandraDateCorrectly() { + + Row rowMock = RowMockUtil.newRowMock(column("id", "my-id", DataType.ascii()), + column("localDate", LocalDate.fromYearMonthDay(2010, 7, 4), DataType.date())); + + TypeWithThreeTenBpLocalDateMappedToDate result = mappingCassandraConverter + .readRow(TypeWithThreeTenBpLocalDateMappedToDate.class, rowMock); + + assertThat(result.localDate, is(notNullValue())); + assertThat(result.localDate.getYear(), is(2010)); + assertThat(result.localDate.getMonthValue(), is(7)); + assertThat(result.localDate.getDayOfMonth(), is(4)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateInsertWithThreeTenBpLocalDateUsingCassandraDateCorrectly() { + + TypeWithThreeTenBpLocalDateMappedToDate typeWithLocalDate = new TypeWithThreeTenBpLocalDateMappedToDate(); + typeWithLocalDate.localDate = org.threeten.bp.LocalDate.of(2010, 7, 4); + + Insert insert = QueryBuilder.insertInto("table"); + mappingCassandraConverter.write(typeWithLocalDate, insert); + + assertThat(getValues(insert), contains((Object) LocalDate.fromYearMonthDay(2010, 7, 4))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreateUpdateWithThreeTenBpLocalDateUsingCassandraDateCorrectly() { + + TypeWithThreeTenBpLocalDateMappedToDate typeWithLocalDate = new TypeWithThreeTenBpLocalDateMappedToDate(); + typeWithLocalDate.localDate = org.threeten.bp.LocalDate.of(2010, 7, 4); + + Update update = QueryBuilder.update("table"); + mappingCassandraConverter.write(typeWithLocalDate, update); + + assertThat(getAssignmentValues(update), contains((Object) LocalDate.fromYearMonthDay(2010, 7, 4))); + } + + @SuppressWarnings("unchecked") private List getValues(Insert statement) { return (List) ReflectionTestUtils.getField(statement, "values"); @@ -565,4 +878,65 @@ public class MappingCassandraConverterUnitTests { public static enum Condition { MINT, USED; } + + @Table + public static class TypeWithLocalDate { + + @PrimaryKey private String id; + + java.time.LocalDate localDate; + java.time.LocalDateTime localDateTime; + + List list; + Set set; + } + + /** + * Uses Cassandra's {@link Name#DATE} which maps by default to {@link LocalDate} + */ + @Table + public static class TypeWithLocalDateMappedToDate { + + @PrimaryKey private String id; + + @CassandraType(type = Name.DATE) java.time.LocalDate localDate; + } + + /** + * Uses Cassandra's {@link Name#DATE} which maps by default to Joda {@link LocalDate} + */ + @Table + public static class TypeWithJodaLocalDateMappedToDate { + + @PrimaryKey private String id; + + @CassandraType(type = Name.DATE) org.joda.time.LocalDate localDate; + } + + /** + * Uses Cassandra's {@link Name#DATE} which maps by default to Joda {@link LocalDate} + */ + @Table + public static class TypeWithThreeTenBpLocalDateMappedToDate { + + @PrimaryKey private String id; + + @CassandraType(type = Name.DATE) org.threeten.bp.LocalDate localDate; + } + + @Table + public static class TypeWithInstant { + + @PrimaryKey private String id; + + Instant instant; + } + + @Table + public static class TypeWithZoneId { + + @PrimaryKey private String id; + + ZoneId zoneId; + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/AllPossibleTypes.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/AllPossibleTypes.java index 680b34dd0..c0b0a87f8 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/AllPossibleTypes.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/AllPossibleTypes.java @@ -29,8 +29,10 @@ import org.springframework.data.cassandra.mapping.CassandraType; import org.springframework.data.cassandra.mapping.PrimaryKey; import org.springframework.data.cassandra.mapping.Table; import org.springframework.data.cassandra.test.integration.mapping.types.CassandraTypeMappingIntegrationTest.Condition; +import org.threeten.bp.LocalDateTime; import com.datastax.driver.core.DataType.Name; +import com.datastax.driver.core.LocalDate; import lombok.Data; import lombok.NoArgsConstructor; @@ -88,4 +90,23 @@ public class AllPossibleTypes { private Map mapOfString; private Condition anEnum; + + // supported by conversion + java.time.LocalDate localDate; + java.time.LocalDateTime localDateTime; + java.time.LocalTime localTime; + java.time.Instant instant; + java.time.ZoneId zoneId; + + org.joda.time.LocalDate jodaLocalDate; + org.joda.time.LocalDateTime jodaLocalDateTime; + org.joda.time.DateTime jodaDateTime; + org.joda.time.DateMidnight jodaDateMidnight; + + org.threeten.bp.LocalDate bpLocalDate; + org.threeten.bp.LocalDateTime bpLocalDateTime; + org.threeten.bp.LocalTime bpLocalTime; + org.threeten.bp.Instant bpInstant; + org.threeten.bp.ZoneId bpZoneId; + } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java new file mode 100644 index 000000000..92eccf73f --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/domain/Person.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.mapping.Table; + +import lombok.Data; + +/** + * @author Mark Paluch + */ +@Table +@Data +public class Person { + + @Id String id; + String firstname; +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateTableSpecificationBasicCassandraMappingContextUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateTableSpecificationBasicCassandraMappingContextUnitTests.java new file mode 100644 index 000000000..56cbd400e --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/CreateTableSpecificationBasicCassandraMappingContextUnitTests.java @@ -0,0 +1,385 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.mapping; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.codehaus.jackson.map.ObjectMapper; +import org.junit.Before; +import org.junit.Test; +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.cassandra.core.keyspace.ColumnSpecification; +import org.springframework.cassandra.core.keyspace.CreateTableSpecification; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.convert.CustomConversions; +import org.springframework.data.cassandra.domain.AllPossibleTypes; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.util.StringUtils; + +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.DataType.CollectionType; +import com.datastax.driver.core.DataType.Name; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Unit tests for {@link BasicCassandraMappingContext} targeted on {@link CreateTableSpecification}. + * + * @author Mark Paluch + * @soundtrack Black Rose - Volbeat + */ +public class CreateTableSpecificationBasicCassandraMappingContextUnitTests { + + BasicCassandraMappingContext ctx = new BasicCassandraMappingContext(); + + @Before + public void setUp() throws Exception { + + List> converters = new ArrayList>(); + converters.add(new PersonReadConverter()); + converters.add(new PersonWriteConverter()); + + CustomConversions customConversions = new CustomConversions(converters); + ctx.setCustomConversions(customConversions); + } + + /** + * @see DATACASS-296 + */ + @Test + public void customConversionTestShouldCreateCorrectTableDefinition() { + + CassandraPersistentEntity persistentEntity = ctx.getPersistentEntity(Employee.class); + + CreateTableSpecification specification = ctx.getCreateTableSpecificationFor(persistentEntity); + + assertThat(getColumn("human", specification).getType(), is(DataType.varchar())); + + ColumnSpecification friends = getColumn("friends", specification); + assertThat(friends.getType().isCollection(), is(true)); + + CollectionType friendsCollection = (CollectionType) friends.getType(); + assertThat(friendsCollection.getName(), is(Name.LIST)); + assertThat(friendsCollection.getTypeArguments().size(), is(1)); + assertThat(friendsCollection.getTypeArguments().get(0), is(DataType.varchar())); + + ColumnSpecification people = getColumn("people", specification); + assertThat(people.getType().isCollection(), is(true)); + + CollectionType peopleCollection = (CollectionType) people.getType(); + assertThat(peopleCollection.getName(), is(Name.SET)); + assertThat(peopleCollection.getTypeArguments().size(), is(1)); + assertThat(peopleCollection.getTypeArguments().get(0), is(DataType.varchar())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void customConversionTestShouldHonorTypeAnnotationAndCreateCorrectTableDefinition() { + + CassandraPersistentEntity persistentEntity = ctx.getPersistentEntity(Employee.class); + + CreateTableSpecification specification = ctx.getCreateTableSpecificationFor(persistentEntity); + + assertThat(getColumn("floater", specification).getType(), is(DataType.cfloat())); + + ColumnSpecification enemies = getColumn("enemies", specification); + assertThat(enemies.getType().isCollection(), is(true)); + + CollectionType enemiesCollection = (CollectionType) enemies.getType(); + assertThat(enemiesCollection.getName(), is(Name.SET)); + assertThat(enemiesCollection.getTypeArguments().size(), is(1)); + assertThat(enemiesCollection.getTypeArguments().get(0), is(DataType.bigint())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToVarchar() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("id", specification).getType(), is(DataType.varchar())); + assertThat(getColumn("zoneId", specification).getType(), is(DataType.varchar())); + assertThat(getColumn("bpZoneId", specification).getType(), is(DataType.varchar())); + assertThat(getColumn("anEnum", specification).getType(), is(DataType.varchar())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToTinyInt() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("boxedByte", specification).getType(), is(DataType.tinyint())); + assertThat(getColumn("primitiveByte", specification).getType(), is(DataType.tinyint())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToSmallInt() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("boxedShort", specification).getType(), is(DataType.smallint())); + assertThat(getColumn("primitiveShort", specification).getType(), is(DataType.smallint())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToBigInt() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("boxedLong", specification).getType(), is(DataType.bigint())); + assertThat(getColumn("primitiveLong", specification).getType(), is(DataType.bigint())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToVarInt() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("bigInteger", specification).getType(), is(DataType.varint())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToDecimal() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("bigDecimal", specification).getType(), is(DataType.decimal())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToInt() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("boxedInteger", specification).getType(), is(DataType.cint())); + assertThat(getColumn("primitiveInteger", specification).getType(), is(DataType.cint())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToFloat() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("boxedFloat", specification).getType(), is(DataType.cfloat())); + assertThat(getColumn("primitiveFloat", specification).getType(), is(DataType.cfloat())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToDouble() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("boxedDouble", specification).getType(), is(DataType.cdouble())); + assertThat(getColumn("primitiveDouble", specification).getType(), is(DataType.cdouble())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToBoolean() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("boxedBoolean", specification).getType(), is(DataType.cboolean())); + assertThat(getColumn("primitiveBoolean", specification).getType(), is(DataType.cboolean())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToDate() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("date", specification).getType(), is(DataType.date())); + assertThat(getColumn("localDate", specification).getType(), is(DataType.date())); + assertThat(getColumn("jodaLocalDate", specification).getType(), is(DataType.date())); + assertThat(getColumn("jodaDateMidnight", specification).getType(), is(DataType.date())); + assertThat(getColumn("bpLocalDate", specification).getType(), is(DataType.date())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToTimestamp() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("timestamp", specification).getType(), is(DataType.timestamp())); + assertThat(getColumn("localDateTime", specification).getType(), is(DataType.timestamp())); + assertThat(getColumn("instant", specification).getType(), is(DataType.timestamp())); + assertThat(getColumn("jodaLocalDateTime", specification).getType(), is(DataType.timestamp())); + assertThat(getColumn("jodaDateTime", specification).getType(), is(DataType.timestamp())); + assertThat(getColumn("bpLocalDateTime", specification).getType(), is(DataType.timestamp())); + assertThat(getColumn("bpInstant", specification).getType(), is(DataType.timestamp())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToTimestampUsingOverrides() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(TypeWithOverrides.class); + + assertThat(getColumn("localDate", specification).getType(), is(DataType.timestamp())); + assertThat(getColumn("jodaLocalDate", specification).getType(), is(DataType.timestamp())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void columnsShouldMapToBlob() { + + CreateTableSpecification specification = getCreateTableSpecificationFor(AllPossibleTypes.class); + + assertThat(getColumn("blob", specification).getType(), is(DataType.blob())); + } + + public CreateTableSpecification getCreateTableSpecificationFor(Class persistentEntityClass) { + + CustomConversions customConversions = new CustomConversions(Collections.EMPTY_LIST); + ctx.setCustomConversions(customConversions); + + CassandraPersistentEntity persistentEntity = ctx.getPersistentEntity(persistentEntityClass); + return ctx.getCreateTableSpecificationFor(persistentEntity); + } + + private ColumnSpecification getColumn(String columnName, CreateTableSpecification specification) { + + for (ColumnSpecification columnSpecification : specification.getColumns()) { + if (columnSpecification.getName().equals(CqlIdentifier.cqlId(columnName))) { + return columnSpecification; + } + } + + throw new IllegalArgumentException( + String.format("Cannot find column '%s' amongst '%s'", columnName, specification.getColumns())); + } + + /** + * @author Mark Paluch + */ + @Data + @Table + public static class Employee { + + @Id String id; + + Human human; + List friends; + Set people; + + @CassandraType(type = Name.FLOAT) Human floater; + @CassandraType(type = Name.SET, typeArguments = Name.BIGINT) List enemies; + } + + /** + * @author Mark Paluch + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + static class Human { + + String firstname; + String lastname; + } + + /** + * @author Mark Paluch + */ + @Data + @Table + static class TypeWithOverrides { + + @Id String id; + + @CassandraType(type = Name.TIMESTAMP) java.time.LocalDate localDate; + + @CassandraType(type = Name.TIMESTAMP) org.joda.time.LocalDate jodaLocalDate; + } + + static class PersonReadConverter implements Converter { + + public Human convert(String source) { + + if (StringUtils.hasText(source)) { + try { + return new ObjectMapper().readValue(source, Human.class); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + return null; + } + } + + static class PersonWriteConverter implements Converter { + + public String convert(Human source) { + + try { + return new ObjectMapper().writeValueAsString(source); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/MappingContextUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/MappingContextUnitTests.java index 5415616ec..f17d1e176 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/MappingContextUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/mapping/MappingContextUnitTests.java @@ -19,12 +19,17 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.Test; import org.springframework.cassandra.core.PrimaryKeyType; import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.convert.CustomConversions; import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.util.ClassTypeInformation; /** * Unit tests for {@link BasicCassandraMappingContext}. @@ -34,107 +39,15 @@ import org.springframework.data.mapping.model.MappingException; */ public class MappingContextUnitTests { - public static class Transient {} - - @Table - public static class X { - @PrimaryKey String key; - } - - @Table - public static class Y { - @PrimaryKey String key; - } - - @Table - public static class PrimaryKeyOnProperty { - - String key; - - @PrimaryKey(value = "foo") - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - } - - @Table - public static class PrimaryKeyColumnsOnProperty { - - String firstname; - String lastname; - - @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED) - public String getFirstname() { - return firstname; - } - - public void setFirstname(String firstname) { - this.firstname = firstname; - } - - @PrimaryKeyColumn(name = "mylastname", ordinal = 2, type = PrimaryKeyType.CLUSTERED) - public String getLastname() { - return lastname; - } - - public void setLastname(String lastname) { - this.lastname = lastname; - } - } - - @Table - public static class PrimaryKeyOnPropertyWithPrimaryKeyClass { - - CompositePrimaryKeyClassWithProperties key; - - @PrimaryKey - public CompositePrimaryKeyClassWithProperties getKey() { - return key; - } - - public void setKey(CompositePrimaryKeyClassWithProperties key) { - this.key = key; - } - } - - @PrimaryKeyClass - public static class CompositePrimaryKeyClassWithProperties implements Serializable{ - - String firstname; - String lastname; - - @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED) - public String getFirstname() { - return firstname; - } - - public void setFirstname(String firstname) { - this.firstname = firstname; - } - - @PrimaryKeyColumn(name = "mylastname", ordinal = 2, type = PrimaryKeyType.CLUSTERED) - public String getLastname() { - return lastname; - } - - public void setLastname(String lastname) { - this.lastname = lastname; - } - } - BasicCassandraMappingContext ctx = new BasicCassandraMappingContext(); @Test(expected = MappingException.class) public void testGetPersistentEntityOfTransientType() { - - CassandraPersistentEntity entity = ctx.getPersistentEntity(Transient.class); - + ctx.getPersistentEntity(Transient.class); } + private static class Transient {} + @Test public void testGetExistingPersistentEntityHappyPath() { @@ -145,6 +58,16 @@ public class MappingContextUnitTests { assertFalse(ctx.contains(Y.class)); } + @Table + private static class X { + @PrimaryKey String key; + } + + @Table + private static class Y { + @PrimaryKey String key; + } + /** * @see DATACASS-248 */ @@ -161,6 +84,21 @@ public class MappingContextUnitTests { assertThat(columnNames.get(0).toCql(), is(equalTo("foo"))); } + @Table + private static class PrimaryKeyOnProperty { + + String key; + + @PrimaryKey(value = "foo") + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + } + /** * @see DATACASS-248 */ @@ -182,32 +120,133 @@ public class MappingContextUnitTests { assertThat(lastname.isClusterKeyColumn(), is(true)); assertThat(lastname.getColumnName().toCql(), is(equalTo("mylastname"))); } - + + @Table + private static class PrimaryKeyColumnsOnProperty { + + String firstname; + String lastname; + + @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED) + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + @PrimaryKeyColumn(name = "mylastname", ordinal = 2, type = PrimaryKeyType.CLUSTERED) + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + } + /** * @see DATACASS-248 */ @Test public void primaryKeyClassWithprimaryKeyColumnsOnPropertyShouldWork() { - CassandraPersistentEntity persistentEntity = ctx.getPersistentEntity(PrimaryKeyOnPropertyWithPrimaryKeyClass.class); - CassandraPersistentEntity primaryKeyClass = ctx.getPersistentEntity(CompositePrimaryKeyClassWithProperties.class); + CassandraPersistentEntity persistentEntity = ctx + .getPersistentEntity(PrimaryKeyOnPropertyWithPrimaryKeyClass.class); + CassandraPersistentEntity primaryKeyClass = ctx + .getPersistentEntity(CompositePrimaryKeyClassWithProperties.class); assertThat(persistentEntity.isCompositePrimaryKey(), is(false)); assertThat(persistentEntity.getPersistentProperty("key").isCompositePrimaryKey(), is(true)); - + assertThat(primaryKeyClass.isCompositePrimaryKey(), is(true)); assertThat(primaryKeyClass.getCompositePrimaryKeyProperties(), hasSize(2)); - + CassandraPersistentProperty firstname = primaryKeyClass.getPersistentProperty("firstname"); assertThat(firstname.isPrimaryKeyColumn(), is(true)); assertThat(firstname.isPartitionKeyColumn(), is(true)); assertThat(firstname.isClusterKeyColumn(), is(false)); assertThat(firstname.getColumnName().toCql(), is(equalTo("firstname"))); - + CassandraPersistentProperty lastname = primaryKeyClass.getPersistentProperty("lastname"); assertThat(lastname.isPrimaryKeyColumn(), is(true)); assertThat(lastname.isPartitionKeyColumn(), is(false)); assertThat(lastname.isClusterKeyColumn(), is(true)); assertThat(lastname.getColumnName().toCql(), is(equalTo("mylastname"))); } + + @Table + private static class PrimaryKeyOnPropertyWithPrimaryKeyClass { + + CompositePrimaryKeyClassWithProperties key; + + @PrimaryKey + public CompositePrimaryKeyClassWithProperties getKey() { + return key; + } + + public void setKey(CompositePrimaryKeyClassWithProperties key) { + this.key = key; + } + } + + @PrimaryKeyClass + private static class CompositePrimaryKeyClassWithProperties implements Serializable { + + String firstname; + String lastname; + + @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED) + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + @PrimaryKeyColumn(name = "mylastname", ordinal = 2, type = PrimaryKeyType.CLUSTERED) + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldCreatePersistentEntityIfNoConversionRegistered() { + + ctx.setCustomConversions(new CustomConversions(Collections.EMPTY_LIST)); + assertThat(ctx.shouldCreatePersistentEntityFor(ClassTypeInformation.from(Human.class)), is(true)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldNotCreateEntitiesForCustomConvertedTypes() { + + List converters = Arrays.asList(new HumanToStringConverter()); + ctx.setCustomConversions(new CustomConversions(converters)); + + assertThat(ctx.shouldCreatePersistentEntityFor(ClassTypeInformation.from(Human.class)), is(false)); + } + + private static class Human { + + } + + private static class HumanToStringConverter implements Converter { + + @Override + public String convert(Human source) { + return "hello"; + } + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryQueryMethodParameterTypesIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryQueryMethodParameterTypesIntegrationTests.java new file mode 100644 index 000000000..82afd4c4c --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryQueryMethodParameterTypesIntegrationTests.java @@ -0,0 +1,232 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.isolated; + +import static java.time.Instant.*; +import static java.time.ZoneId.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.convert.CustomConversions; +import org.springframework.data.cassandra.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.domain.AllPossibleTypes; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.cassandra.repository.Query; +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.data.repository.CrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.datastax.driver.core.DataType.Name; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.exceptions.InvalidQueryException; + +/** + * Integration tests for various query method parameter types. + * + * @author Mark Paluch + * @see DATACASS-296 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@SuppressWarnings("Since15") +public class RepositoryQueryMethodParameterTypesIntegrationTests + extends AbstractSpringDataEmbeddedCassandraIntegrationTest { + + @Configuration + @EnableCassandraRepositories(basePackageClasses = RepositoryQueryMethodParameterTypesIntegrationTests.class, + considerNestedRepositories = true) + public static class Config extends IntegrationTestConfig { + + @Override + public String[] getEntityBasePackages() { + return new String[] { AllPossibleTypes.class.getPackage().getName() }; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE_DROP_UNUSED; + } + } + + @Autowired AllPossibleTypesRepository allPossibleTypesRepository; + @Autowired Session session; + @Autowired BasicCassandraMappingContext mappingContext; + @Autowired MappingCassandraConverter converter; + + @Before + public void setUp() throws Exception { + allPossibleTypesRepository.deleteAll(); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldFindByLocalDate() { + + session.execute("CREATE INDEX IF NOT EXISTS allpossibletypes_localdate ON allpossibletypes ( localdate )"); + + AllPossibleTypes allPossibleTypes = new AllPossibleTypes(); + + allPossibleTypes.setId("id"); + allPossibleTypes.setLocalDate(LocalDate.now()); + + allPossibleTypesRepository.save(allPossibleTypes); + + List result = allPossibleTypesRepository.findWithCreatedDate(allPossibleTypes.getLocalDate()); + + assertThat(result, hasSize(1)); + assertThat(result, contains(allPossibleTypes)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldFindByAnnotatedDateParameter() { + + CustomConversions customConversions = new CustomConversions(Arrays.asList(new DateToLocalDateConverter())); + + mappingContext.setCustomConversions(customConversions); + converter.setCustomConversions(customConversions); + converter.afterPropertiesSet(); + + session.execute("CREATE INDEX IF NOT EXISTS allpossibletypes_date ON allpossibletypes ( date )"); + + AllPossibleTypes allPossibleTypes = new AllPossibleTypes(); + + LocalDate localDate = LocalDate.now(); + Instant instant = localDate.atStartOfDay().toInstant(ZoneOffset.UTC); + + allPossibleTypes.setId("id"); + allPossibleTypes.setDate(com.datastax.driver.core.LocalDate.fromYearMonthDay(localDate.getYear(), + localDate.getMonthValue(), localDate.getDayOfMonth())); + + allPossibleTypesRepository.save(allPossibleTypes); + + List result = allPossibleTypesRepository.findWithAnnotatedDateParameter(Date.from(instant)); + + assertThat(result, hasSize(1)); + assertThat(result, contains(allPossibleTypes)); + } + + /** + * @see DATACASS-296 + */ + @Test(expected = InvalidQueryException.class) + public void shouldThrowExceptionUsingWrongMethodParameter() { + + // NOTE: InvalidQueryException is a driver exception. This should get fixed with DATACASS-304 + + session.execute("CREATE INDEX IF NOT EXISTS allpossibletypes_date ON allpossibletypes ( date )"); + allPossibleTypesRepository.findWithDateParameter(Date.from(Instant.ofEpochSecond(44234123421L))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldFindByZoneId() { + + ZoneId zoneId = ZoneId.of("Europe/Paris"); + session.execute("CREATE INDEX IF NOT EXISTS allpossibletypes_zoneid ON allpossibletypes ( zoneid )"); + + AllPossibleTypes allPossibleTypes = new AllPossibleTypes(); + + allPossibleTypes.setId("id"); + allPossibleTypes.setZoneId(zoneId); + + allPossibleTypesRepository.save(allPossibleTypes); + + List result = allPossibleTypesRepository.findWithZoneId(zoneId); + + assertThat(result, hasSize(1)); + assertThat(result, contains(allPossibleTypes)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldFindByOptionalOfZoneId() { + + ZoneId zoneId = ZoneId.of("Europe/Paris"); + session.execute("CREATE INDEX IF NOT EXISTS allpossibletypes_zoneid ON allpossibletypes ( zoneid )"); + + AllPossibleTypes allPossibleTypes = new AllPossibleTypes(); + + allPossibleTypes.setId("id"); + allPossibleTypes.setZoneId(zoneId); + + allPossibleTypesRepository.save(allPossibleTypes); + + List result = allPossibleTypesRepository.findWithZoneId(Optional.of(zoneId)); + + assertThat(result, hasSize(1)); + assertThat(result, contains(allPossibleTypes)); + } + + private interface AllPossibleTypesRepository extends CrudRepository { + + @Query("select * from allpossibletypes where localdate = ?0") + List findWithCreatedDate(java.time.LocalDate createdDate); + + @Query("select * from allpossibletypes where zoneid = ?0") + List findWithZoneId(ZoneId zoneId); + + @Query("select * from allpossibletypes where date = ?0") + List findWithAnnotatedDateParameter(@CassandraType(type = Name.DATE) Date timestamp); + + @Query("select * from allpossibletypes where date = ?0") + List findWithDateParameter(Date timestamp); + + @Query("select * from allpossibletypes where zoneid = ?0") + List findWithZoneId(Optional zoneId); + } + + private static class DateToLocalDateConverter implements Converter { + + @Override + public com.datastax.driver.core.LocalDate convert(Date source) { + + LocalDate localDate = LocalDateTime.ofInstant(ofEpochMilli(source.getTime()), systemDefault()).toLocalDate(); + return com.datastax.driver.core.LocalDate.fromYearMonthDay(localDate.getYear(), localDate.getMonthValue(), + localDate.getDayOfMonth()); + } + } + +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryReturnTypesIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryReturnTypesIntegrationTests.java index 52934a8fc..f3e01134c 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryReturnTypesIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/isolated/RepositoryReturnTypesIntegrationTests.java @@ -315,7 +315,7 @@ public class RepositoryReturnTypesIntegrationTests extends AbstractSpringDataEmb allPossibleTypesRepository.save(entity); Map result = allPossibleTypesRepository.findEntityAsMapById(entity.getId()); - assertThat(result.size(), is(27)); + assertThat(result.size(), is(41)); assertThat(result.get("primitiveinteger"), is(equalTo((Object) Integer.valueOf(123)))); assertThat(result.get("biginteger"), is(equalTo((Object) BigInteger.ONE))); } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessorUnitTests.java new file mode 100644 index 000000000..f3234c754 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessorUnitTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.cassandra.domain.AllPossibleTypes; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.threeten.bp.LocalDateTime; + +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.DataType.Name; + +/** + * Unit tests for {@link CassandraParametersParameterAccessor}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class CassandraParametersParameterAccessorUnitTests { + + @Mock ProjectionFactory projectionFactory; + + RepositoryMetadata metadata = new DefaultRepositoryMetadata(PossibleRepository.class); + CassandraMappingContext context = new BasicCassandraMappingContext(); + + /** + * @see DATACASS-296 + */ + @Test + public void returnsCassandraSimpleType() throws Exception { + + Method method = PossibleRepository.class.getMethod("findByFirstname", String.class); + CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(getCassandraQueryMethod(method), + new Object[] { "firstname" }); + + assertThat(accessor.getDataType(0), is(equalTo(DataType.varchar()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReturnNoTypeForComplexTypes() throws Exception { + + Method method = PossibleRepository.class.getMethod("findByBpLocalDateTime", LocalDateTime.class); + CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(getCassandraQueryMethod(method), + new Object[] { LocalDateTime.of(2000, 10, 11, 12, 13, 14) }); + + assertThat(accessor.getDataType(0), is(nullValue())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void returnTypeForAnnotatedParameter() throws Exception { + + Method method = PossibleRepository.class.getMethod("findByAnnotatedBpLocalDateTime", LocalDateTime.class); + CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(getCassandraQueryMethod(method), + new Object[] { LocalDateTime.of(2000, 10, 11, 12, 13, 14) }); + + assertThat(accessor.getDataType(0), is(DataType.date())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void returnTypeForAnnotatedParameterWhenUsingStringValue() throws Exception { + + Method method = PossibleRepository.class.getMethod("findByAnnotatedObject", Object.class); + CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(getCassandraQueryMethod(method), + new Object[] { "" }); + + assertThat(accessor.getDataType(0), is(DataType.date())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void returnTypeForAnnotatedParameterWhenUsingNullValue() throws Exception { + + Method method = PossibleRepository.class.getMethod("findByAnnotatedObject", Object.class); + CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(getCassandraQueryMethod(method), + new Object[] { "" }); + + assertThat(accessor.getDataType(0), is(DataType.date())); + } + + private CassandraQueryMethod getCassandraQueryMethod(Method method) { + return new CassandraQueryMethod(method, metadata, projectionFactory, context); + } + + interface PossibleRepository extends Repository { + + List findByFirstname(String firstname); + + List findByBpLocalDateTime(LocalDateTime dateTime); + + List findByAnnotatedBpLocalDateTime(@CassandraType(type = Name.DATE) LocalDateTime dateTime); + + List findByAnnotatedObject(@CassandraType(type = Name.DATE) Object dateTime); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java new file mode 100644 index 000000000..965eeed6a --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraParametersUnitTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Method; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.cassandra.domain.Person; +import org.springframework.data.cassandra.mapping.CassandraType; + +import com.datastax.driver.core.DataType; +import com.datastax.driver.core.DataType.Name; + +/** + * Unit tests for {@link CassandraParameters}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class CassandraParametersUnitTests { + + @Mock CassandraQueryMethod queryMethod; + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReturnDataTypeForSimpleType() throws Exception { + + Method method = PersonRepository.class.getMethod("findByFirstname", String.class); + CassandraParameters cassandraParameters = new CassandraParameters(method); + + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(DataType.varchar())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReturnDataTypeForAnnotatedSimpleType() throws Exception { + + Method method = PersonRepository.class.getMethod("findByFirstTime", String.class); + CassandraParameters cassandraParameters = new CassandraParameters(method); + + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(DataType.time())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReturnNoTypeForComplexType() throws Exception { + + Method method = PersonRepository.class.getMethod("findByObject", Object.class); + CassandraParameters cassandraParameters = new CassandraParameters(method); + + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(nullValue())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReturnTypeForAnnotatedType() throws Exception { + + Method method = PersonRepository.class.getMethod("findByAnnotatedObject", Object.class); + CassandraParameters cassandraParameters = new CassandraParameters(method); + + assertThat(cassandraParameters.getParameter(0).getCassandraType(), is(DataType.time())); + } + + interface PersonRepository { + + Person findByFirstname(String firstname); + + Person findByFirstTime(@CassandraType(type = Name.TIME) String firstname); + + Person findByObject(Object firstname); + + Person findByAnnotatedObject(@CassandraType(type = Name.TIME) Object firstname); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java new file mode 100644 index 000000000..649cddf53 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessorUnitTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDate; + +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.MappingCassandraConverter; +import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; + +import com.datastax.driver.core.DataType; + +/** + * Unit tests for {@link ConvertingParameterAccessor}. + * + * @author Mark Paluch + */ +@SuppressWarnings("Since15") +@RunWith(MockitoJUnitRunner.class) +public class ConvertingParameterAccessorUnitTests { + + @Mock CassandraParameterAccessor delegateMock; + + MappingCassandraConverter converter; + + @Before + public void setUp() { + + this.converter = new MappingCassandraConverter(new BasicCassandraMappingContext()); + this.converter.afterPropertiesSet(); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReturnNullBindableValue() { + + ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, delegateMock); + + assertThat(accessor.getBindableValue(0), is(nullValue())); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReturnNativeBindableValue() { + + ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, delegateMock); + + when(delegateMock.getBindableValue(0)).thenReturn("hello"); + when(delegateMock.getDataType(0)).thenReturn(DataType.varchar()); + + assertThat(accessor.getBindableValue(0), is(equalTo((Object) "hello"))); + } + + /** + * @see DATACASS-296 + */ + @Test + @SuppressWarnings("rawtypes") + public void shouldReturnConvertedBindableValue() { + + ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, delegateMock); + + LocalDate localDate = LocalDate.of(2010, 7, 4); + + when(delegateMock.getBindableValue(0)).thenReturn(localDate); + when(delegateMock.getParameterType(0)).thenReturn((Class) LocalDate.class); + + assertThat(accessor.getBindableValue(0), + is(equalTo((Object) com.datastax.driver.core.LocalDate.fromYearMonthDay(2010, 7, 4)))); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/StringBasedCassandraQueryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java similarity index 72% rename from spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/StringBasedCassandraQueryIntegrationTests.java rename to spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java index 28b78e1aa..388f0458c 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/StringBasedCassandraQueryIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.cassandra.test.integration.repository.querymethods.declared; +package org.springframework.data.cassandra.repository.query; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; +import java.time.LocalDate; import org.junit.Before; import org.junit.Test; @@ -31,9 +32,7 @@ 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.cassandra.test.integration.repository.querymethods.declared.Person; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; @@ -49,14 +48,15 @@ import com.datastax.driver.core.querybuilder.Select; * * @author Matthew T. Adams * @author Oliver Gierke + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) -public class StringBasedCassandraQueryIntegrationTests { +public class StringBasedCassandraQueryIntegrationUnitTests { @Mock CassandraOperations operations; RepositoryMetadata metadata; - CassandraConverter converter; + MappingCassandraConverter converter; ProjectionFactory factory; @Before @@ -67,6 +67,8 @@ public class StringBasedCassandraQueryIntegrationTests { this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); this.converter = new MappingCassandraConverter(new BasicCassandraMappingContext()); this.factory = new SpelAwareProxyProjectionFactory(); + + this.converter.afterPropertiesSet(); } @Test @@ -96,10 +98,10 @@ public class StringBasedCassandraQueryIntegrationTests { CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory, converter.getMappingContext()); StringBasedCassandraQuery cassandraQuery = new StringBasedCassandraQuery(queryMethod, operations); - CassandraParametersParameterAccessor accesor = new CassandraParametersParameterAccessor(queryMethod, "Matthews", + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor(queryMethod, "Matthews", "John"); - String stringQuery = cassandraQuery.createQuery(accesor); + String stringQuery = cassandraQuery.createQuery(accessor); SimpleStatement actual = new SimpleStatement(stringQuery); String table = Person.class.getSimpleName().toLowerCase(); @@ -110,6 +112,30 @@ public class StringBasedCassandraQueryIntegrationTests { assertThat(actual.getQueryString(), is(expected.getQueryString())); } + /** + * @see DATACASS-296 + */ + @Test + public void bindsConvertedPropertyCorrectly() throws Exception { + + Method method = SampleRepository.class.getMethod("findByCreatedDate", LocalDate.class); + CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory, + converter.getMappingContext()); + StringBasedCassandraQuery cassandraQuery = new StringBasedCassandraQuery(queryMethod, operations); + CassandraParameterAccessor accessor = new ConvertingParameterAccessor(converter, new CassandraParametersParameterAccessor(queryMethod, + LocalDate.of(2010, 7, 4))); + + String stringQuery = cassandraQuery.createQuery(accessor); + SimpleStatement actual = new SimpleStatement(stringQuery); + + String table = Person.class.getSimpleName().toLowerCase(); + Select expected = QueryBuilder.select().all().from(table); + expected.setForceNoValues(true); + expected.where(QueryBuilder.eq("createdDate", com.datastax.driver.core.LocalDate.fromYearMonthDay(2010, 7, 4))); + + assertThat(actual.getQueryString(), is(expected.getQueryString())); + } + private interface SampleRepository extends Repository { @Query("SELECT * FROM person WHERE lastname=?0;") @@ -117,5 +143,8 @@ public class StringBasedCassandraQueryIntegrationTests { @Query("SELECT * FROM person WHERE lastname=?0 AND firstname=?1;") Person findByLastnameAndFirstname(String lastname, String firstname); + + @Query("SELECT * FROM person WHERE createdDate=?0;") + Person findByCreatedDate(LocalDate createdDate); } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/config/SchemaActionIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/config/SchemaActionIntegrationTests.java index cda4a7ab4..d94aaf85b 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/config/SchemaActionIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/config/SchemaActionIntegrationTests.java @@ -46,38 +46,25 @@ import com.datastax.driver.core.Session; import com.datastax.driver.core.TableMetadata; /** - * The SchemaActionIntegrationTests class is a test suite of test cases testing the contract and behavior - * of various {@link SchemaAction}s on startup of a Spring configured, Cassandra application client. + * The SchemaActionIntegrationTests class is a test suite of test cases testing the contract and behavior of various + * {@link SchemaAction}s on startup of a Spring configured, Cassandra application client. * * @author John Blum - * @see org.springframework.cassandra.test.integration.AbstractEmbeddedCassandraIntegrationTest - * @see org.springframework.cassandra.test.integration.KeyspaceRule - * @see org.springframework.data.cassandra.config.CassandraSessionFactoryBean - * @see org.springframework.data.cassandra.config.SchemaAction * @see T[] asArray(T... array) { - return array; - } + @Rule public KeyspaceRule KEYSPACE_RULE = new KeyspaceRule(cassandraEnvironment, KEYSPACE_NAME); protected ConfigurableApplicationContext newApplicationContext(Class... annotatedClasses) { - AnnotationConfigApplicationContext applicationContext = - new AnnotationConfigApplicationContext(annotatedClasses); + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(annotatedClasses); applicationContext.registerShutdownHook(); @@ -91,13 +78,9 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg applicationContext = newApplicationContext(annotatedClass); return sessionCallback.doInSession(applicationContext.getBean(Session.class)); } finally { - close(applicationContext); - } - } - - protected void close(ConfigurableApplicationContext applicationContext) { - if (applicationContext != null) { - applicationContext.close(); + if (applicationContext != null) { + applicationContext.close(); + } } } @@ -114,22 +97,21 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg assertThat(tableMetadata.getColumns().size(), is(equalTo(columns.length))); for (String columnName : columns) { - assertThat(String.format("Column [%s] dos not exist!", columnName), - tableMetadata.getColumn(columnName), is(notNullValue())); + assertThat(String.format("Column [%s] dos not exist!", columnName), tableMetadata.getColumn(columnName), + is(notNullValue())); } } @Test public void createWithNoExistingTableCreatesTableFromEntity() { - doInSessionWithConfiguration(CreateWithNoExistingTableConfiguration.class, - new SessionCallback() { - @Override public Void doInSession(Session session) throws DataAccessException { - assertHasTableWithColumns(session, "person", "firstName", "lastName", "nickname", - "birthDate", "numberOfChildren", "cool"); - return null; - } + doInSessionWithConfiguration(CreateWithNoExistingTableConfiguration.class, new SessionCallback() { + @Override + public Void doInSession(Session session) throws DataAccessException { + assertHasTableWithColumns(session, "person", "firstName", "lastName", "nickname", "birthDate", + "numberOfChildren", "cool", "createdDate", "zoneId"); + return null; } - ); + }); } @Test @@ -137,53 +119,48 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg exception.expect(BeanCreationException.class); exception.expectMessage(containsString(String.format("Table %s.person already exists", KEYSPACE_NAME))); - doInSessionWithConfiguration(CreateWithExistingTableConfiguration.class, - new SessionCallback() { - @Override public Object doInSession(Session s) throws DataAccessException { - fail(String.format("%s should have failed!", - CreateWithExistingTableConfiguration.class.getSimpleName())); - return null; - } + doInSessionWithConfiguration(CreateWithExistingTableConfiguration.class, new SessionCallback() { + @Override + public Object doInSession(Session s) throws DataAccessException { + fail(String.format("%s should have failed!", CreateWithExistingTableConfiguration.class.getSimpleName())); + return null; } - ); + }); } @Test public void createIfNotExistsWithNoExistingTableCreatesTableFromEntity() { - doInSessionWithConfiguration(CreateIfNotExistsWithNoExistingTableConfiguration.class, - new SessionCallback() { - @Override public Void doInSession(Session session) throws DataAccessException { - assertHasTableWithColumns(session, "person", "firstName", "lastName", "nickname", - "birthDate", "numberOfChildren", "cool"); - return null; - } + doInSessionWithConfiguration(CreateIfNotExistsWithNoExistingTableConfiguration.class, new SessionCallback() { + @Override + public Void doInSession(Session session) throws DataAccessException { + assertHasTableWithColumns(session, "person", "firstName", "lastName", "nickname", "birthDate", + "numberOfChildren", "cool", "createdDate", "zoneId"); + return null; } - ); + }); } @Test public void createIfNotExistsWithExistingTableUsesExistingTable() { - doInSessionWithConfiguration(CreateIfNotExistsWithExistingTableConfiguration.class, - new SessionCallback() { - @Override public Void doInSession(Session session) throws DataAccessException { - assertHasTableWithColumns(session, "person", "id", "firstName", "lastName"); - return null; - } + doInSessionWithConfiguration(CreateIfNotExistsWithExistingTableConfiguration.class, new SessionCallback() { + @Override + public Void doInSession(Session session) throws DataAccessException { + assertHasTableWithColumns(session, "person", "id", "firstName", "lastName"); + return null; } - ); + }); } @Test public void recreateTableFromEntityDropsExistingTable() { - doInSessionWithConfiguration(RecreateSchemaActionWithExistingTableConfiguration.class, - new SessionCallback() { - @Override public Void doInSession(Session session) throws DataAccessException { - assertHasTableWithColumns(session, "person", "firstName", "lastName", "nickname", - "birthDate", "numberOfChildren", "cool"); - return null; - } + doInSessionWithConfiguration(RecreateSchemaActionWithExistingTableConfiguration.class, new SessionCallback() { + @Override + public Void doInSession(Session session) throws DataAccessException { + assertHasTableWithColumns(session, "person", "firstName", "lastName", "nickname", "birthDate", + "numberOfChildren", "cool", "createdDate", "zoneId"); + return null; } - ); + }); } @Configuration @@ -253,11 +230,13 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg @Override public CassandraCqlClusterFactoryBean cluster() { return new CassandraCqlClusterFactoryBean() { - @Override public void afterPropertiesSet() throws Exception { + @Override + public void afterPropertiesSet() throws Exception { // avoid Cassandra Cluster creation; use embedded } - @Override public Cluster getObject() { + @Override + public Cluster getObject() { return cassandraEnvironment.getCluster(); } }; @@ -265,7 +244,7 @@ public class SchemaActionIntegrationTests extends AbstractEmbeddedCassandraInteg @Override public String[] getEntityBasePackages() { - return asArray(Person.class.getPackage().getName()); + return new String[] { Person.class.getPackage().getName() }; } @Override diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/customconversion/CustomConversionTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/customconversion/CustomConversionTests.java new file mode 100644 index 000000000..27ea39eee --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/customconversion/CustomConversionTests.java @@ -0,0 +1,290 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.test.integration.mapping.customconversion; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.codehaus.jackson.map.ObjectMapper; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.convert.CustomConversions; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.mapping.Table; +import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; +import org.springframework.data.cassandra.test.integration.repository.querymethods.datekey.DateThingRepo; +import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.StringUtils; + +import com.datastax.driver.core.Row; +import com.datastax.driver.core.querybuilder.QueryBuilder; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Test suite for applying {@link CustomConversions} to + * {@link org.springframework.data.cassandra.mapping.BasicCassandraMappingContext}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class CustomConversionTests { + + @Configuration + @EnableCassandraRepositories(basePackageClasses = DateThingRepo.class) + public static class Config extends IntegrationTestConfig { + + @Override + public String[] getEntityBasePackages() { + return new String[] { Employee.class.getPackage().getName() }; + } + + @Override + public SchemaAction getSchemaAction() { + return SchemaAction.RECREATE_DROP_UNUSED; + } + + @Override + public CustomConversions customConversions() { + + List> converters = new ArrayList>(); + converters.add(new PersonReadConverter()); + converters.add(new PersonWriteConverter()); + + return new CustomConversions(converters); + } + } + + @Autowired CassandraOperations cassandraOperations; + + @Before + public void setUp() { + cassandraOperations.deleteAll(Employee.class); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldInsertCustomConvertedObject() { + + Employee employee = new Employee(); + employee.setId("employee-id"); + employee.setPerson(new Person("Homer", "Simpson")); + + cassandraOperations.insert(employee); + + Row row = cassandraOperations.selectOne(QueryBuilder.select("id", "person").from("employee"), Row.class); + + assertThat(row.getString("id"), is(equalTo("employee-id"))); + assertThat(row.getString("person"), containsString("\"firstname\":\"Homer\"")); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldUpdateCustomConvertedObject() { + + Employee employee = new Employee(); + employee.setId("employee-id"); + + cassandraOperations.insert(employee); + + employee.setPerson(new Person("Homer", "Simpson")); + cassandraOperations.update(employee); + + Row row = cassandraOperations.selectOne(QueryBuilder.select("id", "person").from("employee"), Row.class); + + assertThat(row.getString("id"), is(equalTo("employee-id"))); + assertThat(row.getString("person"), containsString("\"firstname\":\"Homer\"")); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldInsertCustomConvertedObjectWithCollections() { + + Employee employee = new Employee(); + employee.setId("employee-id"); + cassandraOperations.insert(employee); + + employee.setFriends(Arrays.asList(new Person("Carl", "Carlson"), new Person("Lenny", "Leonard"))); + employee.setPeople(Collections.singleton(new Person("Apu", "Nahasapeemapetilon"))); + cassandraOperations.update(employee); + + Row row = cassandraOperations.selectOne(QueryBuilder.select("id", "person", "friends", "people").from("employee"), + Row.class); + + assertThat(row.getObject("friends"), is(instanceOf(List.class))); + assertThat(row.getList("friends", String.class), hasSize(2)); + + assertThat(row.getObject("people"), is(instanceOf(Set.class))); + assertThat(row.getSet("people", String.class), hasSize(1)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldUpdateCustomConvertedObjectWithCollections() { + + Employee employee = new Employee(); + employee.setId("employee-id"); + employee.setFriends(Arrays.asList(new Person("Carl", "Carlson"), new Person("Lenny", "Leonard"))); + employee.setPeople(Collections.singleton(new Person("Apu", "Nahasapeemapetilon"))); + + cassandraOperations.insert(employee); + + Row row = cassandraOperations.selectOne(QueryBuilder.select("id", "person", "friends", "people").from("employee"), + Row.class); + + assertThat(row.getObject("friends"), is(instanceOf(List.class))); + assertThat(row.getList("friends", String.class), hasSize(2)); + + assertThat(row.getObject("people"), is(instanceOf(Set.class))); + assertThat(row.getSet("people", String.class), hasSize(1)); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldLoadCustomConvertedObject() { + + cassandraOperations.execute(QueryBuilder.insertInto("employee").value("id", "employee-id").value("person", + "{\"firstname\":\"Homer\",\"lastname\":\"Simpson\"}")); + + Employee employee = cassandraOperations.selectOne(QueryBuilder.select("id", "person").from("employee"), + Employee.class); + + assertThat(employee.getId(), is(equalTo("employee-id"))); + assertThat(employee.getPerson(), is(notNullValue())); + assertThat(employee.getPerson().getFirstname(), is(equalTo("Homer"))); + assertThat(employee.getPerson().getLastname(), is(equalTo("Simpson"))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldLoadCustomConvertedWithCollectionsObject() { + + cassandraOperations.execute(QueryBuilder.insertInto("employee").value("id", "employee-id").value("people", + Collections.singleton("{\"firstname\":\"Apu\",\"lastname\":\"Nahasapeemapetilon\"}"))); + + Employee employee = cassandraOperations.selectOne(QueryBuilder.select("id", "people").from("employee"), + Employee.class); + + assertThat(employee.getId(), is(equalTo("employee-id"))); + assertThat(employee.getPeople(), is(notNullValue())); + + Person apu = employee.getPeople().iterator().next(); + assertThat(apu.getFirstname(), is(equalTo("Apu"))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void dummy() { + + cassandraOperations.execute(QueryBuilder.insertInto("employee").value("id", "employee-id")); + + cassandraOperations + .execute(QueryBuilder.update("employee").where(QueryBuilder.eq("id", "employee-id")).with(QueryBuilder + .set("people", Collections.singleton("{\"firstname\":\"Apu\",\"lastname\":\"Nahasapeemapetilon\"}")))); + } + + /** + * @author Mark Paluch + */ + @Data + @Table + static class Employee { + + @Id String id; + + Person person; + List friends; + Set people; + } + + /** + * @author Mark Paluch + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + static class Person { + + String firstname; + String lastname; + } + + /** + * @author Mark Paluch + */ + static class PersonReadConverter implements Converter { + + public Person convert(String source) { + + if (StringUtils.hasText(source)) { + try { + return new ObjectMapper().readValue(source, Person.class); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + return null; + } + } + + /** + * @author Mark Paluch + */ + static class PersonWriteConverter implements Converter { + + public String convert(Person source) { + + try { + return new ObjectMapper().writeValueAsString(source); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/types/CassandraTypeMappingIntegrationTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/types/CassandraTypeMappingIntegrationTest.java index 8f620b6c0..5a0270ce8 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/types/CassandraTypeMappingIntegrationTest.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/types/CassandraTypeMappingIntegrationTest.java @@ -56,6 +56,7 @@ import com.datastax.driver.core.querybuilder.QueryBuilder; * @author Mark Paluch * @soundtrack DJ THT meets Scarlet - Live 2 Dance (Extended Mix) (Zgin Remix) */ +@SuppressWarnings("Since15") @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration public class CassandraTypeMappingIntegrationTest extends AbstractSpringDataEmbeddedCassandraIntegrationTest { @@ -521,6 +522,201 @@ public class CassandraTypeMappingIntegrationTest extends AbstractSpringDataEmbed assertThat(loaded.getTime(), is(equalTo(time))); } + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteLocalDate() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setLocalDate(java.time.LocalDate.of(2010, 7, 4)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getLocalDate(), is(equalTo(entity.getLocalDate()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteLocalDateTime() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setLocalDateTime(java.time.LocalDateTime.of(2010, 7, 4, 1, 2, 3)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getLocalDateTime(), is(equalTo(entity.getLocalDateTime()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteLocalTime() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setLocalTime(java.time.LocalTime.of(1, 2, 3)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getLocalTime(), is(equalTo(entity.getLocalTime()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteInstant() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setInstant(java.time.Instant.now()); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getInstant(), is(equalTo(entity.getInstant()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteZoneId() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setZoneId(java.time.ZoneId.of("Europe/Paris")); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getZoneId(), is(equalTo(entity.getZoneId()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteJodaLocalDate() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setJodaLocalDate(new org.joda.time.LocalDate(2010, 7, 4)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getJodaLocalDate(), is(equalTo(entity.getJodaLocalDate()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteJodaDateMidnight() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setJodaDateMidnight(new org.joda.time.DateMidnight(2010, 7, 4)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getJodaDateMidnight(), is(equalTo(entity.getJodaDateMidnight()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteJodaDateTime() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setJodaDateTime(new org.joda.time.DateTime(2010, 7, 4, 1, 2, 3)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getJodaDateTime(), is(equalTo(entity.getJodaDateTime()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteBpLocalDate() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBpLocalDate(org.threeten.bp.LocalDate.of(2010, 7, 4)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBpLocalDate(), is(equalTo(entity.getBpLocalDate()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteBpLocalDateTime() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBpLocalDateTime(org.threeten.bp.LocalDateTime.of(2010, 7, 4, 1, 2, 3)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBpLocalDateTime(), is(equalTo(entity.getBpLocalDateTime()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteBpLocalTime() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBpLocalTime(org.threeten.bp.LocalTime.of(1, 2, 3)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBpLocalTime(), is(equalTo(entity.getBpLocalTime()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteBpInstant() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBpInstant(org.threeten.bp.Instant.now()); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBpZoneId(), is(equalTo(entity.getBpZoneId()))); + } + + /** + * @see DATACASS-296 + */ + @Test + public void shouldReadAndWriteBpZoneId() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBpZoneId(org.threeten.bp.ZoneId.of("Europe/Paris")); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBpZoneId(), is(equalTo(entity.getBpZoneId()))); + } + /** * @see DATACASS-271 */ diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java index 59b355efc..d8c38eae8 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/Person.java @@ -15,14 +15,17 @@ */ package org.springframework.data.cassandra.test.integration.repository.querymethods.declared; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.Date; -import lombok.Data; - import org.springframework.cassandra.core.PrimaryKeyType; +import org.springframework.data.cassandra.mapping.Indexed; import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; import org.springframework.data.cassandra.mapping.Table; +import lombok.Data; + /** * Sample domain class. */ @@ -38,4 +41,7 @@ public class Person { private Date birthDate; private int numberOfChildren; private boolean cool; + + private LocalDate createdDate; + private ZoneId zoneId; } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/QueryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/QueryIntegrationTests.java index 7c74cd436..a78d40eb7 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/QueryIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/QueryIntegrationTests.java @@ -18,6 +18,8 @@ package org.springframework.data.cassandra.test.integration.repository.querymeth import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.Collection; import java.util.Date; import java.util.List; @@ -36,9 +38,14 @@ import org.springframework.data.cassandra.test.integration.support.AbstractSprin import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import com.datastax.driver.core.Session; + /** + * Integration tests for use with {@link PersonRepository}. + * * @author Matthew T. Adams * @author Mark Paluch + * @soundtrack Mary Jane Kelly - Volbeat */ @RunWith(SpringJUnit4ClassRunner.class) public abstract class QueryIntegrationTests extends AbstractSpringDataEmbeddedCassandraIntegrationTest { @@ -57,6 +64,7 @@ public abstract class QueryIntegrationTests extends AbstractSpringDataEmbeddedCa } @Autowired PersonRepository personRepository; + @Autowired Session session; @Before public void before() { @@ -286,8 +294,8 @@ public abstract class QueryIntegrationTests extends AbstractSpringDataEmbeddedCa personToSave = personRepository.save(personToSave); - Optional savedPerson = personRepository.findOptionalWithLastnameAndFirstname( - personToSave.getLastname(), personToSave.getFirstname()); + Optional savedPerson = personRepository.findOptionalWithLastnameAndFirstname(personToSave.getLastname(), + personToSave.getFirstname()); assertThat(savedPerson, is(notNullValue(Optional.class))); assertThat(savedPerson.isPresent(), is(true)); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java index a9a4b0dcb..5325f02cd 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java @@ -15,6 +15,8 @@ */ package org.springframework.data.cassandra.test.integration.repository.querymethods.declared.anno; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.Date; import java.util.List; import java.util.Map; @@ -78,4 +80,5 @@ public interface PersonRepositoryWithQueryAnnotations extends PersonRepository { @Override @Query("select * from person") Stream findAllPeople(); + } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/base/PersonRepository.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/base/PersonRepository.java index a2d40526e..41ee384fa 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/base/PersonRepository.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/base/PersonRepository.java @@ -15,6 +15,8 @@ */ package org.springframework.data.cassandra.test.integration.repository.querymethods.declared.base; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.Date; import java.util.List; import java.util.Map; @@ -55,5 +57,4 @@ public interface PersonRepository extends CassandraRepository { Optional findOptionalWithLastnameAndFirstname(String last, String first); Stream findAllPeople(); - } diff --git a/spring-data-cassandra/src/test/resources/META-INF/PersonRepositoryWithNamedQueries.properties b/spring-data-cassandra/src/test/resources/META-INF/PersonRepositoryWithNamedQueries.properties index 54109186f..c7dc7fc52 100644 --- a/spring-data-cassandra/src/test/resources/META-INF/PersonRepositoryWithNamedQueries.properties +++ b/spring-data-cassandra/src/test/resources/META-INF/PersonRepositoryWithNamedQueries.properties @@ -9,4 +9,7 @@ Person.findSingleCool=select cool from person where lastname = ?0 and firstname Person.findSingleNumberOfChildren=select numberofchildren from person where lastname = ?0 and firstname = ?1 Person.findOptionalWithLastnameAndFirstname=select * from person where lastname = ?0 and firstname = ?1 Person.findAllPeople=select * from person +Person.findPeopleWithCreatedDate=select * from person where createddate = ?0 +Person.findPeopleWithZoneId=select * from person where zoneid = ?0 + diff --git a/spring-data-cassandra/template.mf b/spring-data-cassandra/template.mf index cc8eb300d..9ddff3987 100644 --- a/spring-data-cassandra/template.mf +++ b/spring-data-cassandra/template.mf @@ -7,7 +7,14 @@ Excluded-Imports: Import-Package: sun.reflect;version="0";resolution:=optional Import-Template: + com.datastax.driver.core.*;resolution:="optional";version="[0.1.0, 1.0.0)", + com.google.common.*;resolution:="optional";version="[11.0.0, 20.0.0)", javax.enterprise.*;version="${cdi:[=.=.=,+1.0.0)}";resolution:=optional, + javax.xml.transform.*;resolution:="optional";version="0", + org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.codehaus.jackson.*;resolution:="optional";version="[1.6, 2.0.0)", + org.joda.time.*;version="${jodatime:[=.=.=,+1.0.0)}";resolution:=optional, org.springframework.beans.*;version="[3.1.0, 4.0.0)", org.springframework.cache.*;version="[3.1.0, 4.0.0)", org.springframework.context.*;version="[3.1.0, 4.0.0)", @@ -20,15 +27,6 @@ Import-Template: org.springframework.data.*;version="[1.5.0, 2.0.0)", org.springframework.expression.*;version="[3.1.0, 4.0.0)", org.springframework.cassandra.*;version="[1.0.0,2.0.0)", - org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, - org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", - org.w3c.dom.*;version="0", - javax.xml.transform.*;resolution:="optional";version="0", - com.datastax.driver.core.*;resolution:="optional";version="[0.1.0, 1.0.0)", - org.apache.cassandra.db.marshal.*;version="[1.2.0, 1.3.0)", + org.threeten.bp.*;version="${threetenbp:[=.=.=,+1.0.0)}";resolution:=optional, org.slf4j.*;version="[1.5.0, 1.8.0)", - org.idevlab.rjc.*;resolution:="optional";version="[0.6.4, 0.6.4]", - org.apache.commons.pool.impl.*;resolution:="optional";version="[1.0.0, 3.0.0)", - org.codehaus.jackson.*;resolution:="optional";version="[1.6, 2.0.0)", - org.apache.commons.beanutils.*;resolution:="optional";version=1.8.5, - com.google.common.*;resolution:="optional";version="[11.0.0, 20.0.0)" + org.w3c.dom.*;version="0"