diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraCustomConversions.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraCustomConversions.java index cc470cf73..0d6edee5b 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraCustomConversions.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraCustomConversions.java @@ -17,10 +17,22 @@ package org.springframework.data.cassandra.core.convert; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.core.mapping.CassandraSimpleTypeHolder; +import org.springframework.data.cassandra.core.mapping.CassandraType; +import org.springframework.data.convert.WritingConverter; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.datastax.driver.core.DataType.Name; /** * Value object to capture custom conversion. {@link CassandraCustomConversions} also act as factory for @@ -37,6 +49,12 @@ public class CassandraCustomConversions extends org.springframework.data.convert private static final List STORE_CONVERTERS; + /** + * Set of types that indicate usage of Cassandra's time type. Time is represented as long so we need to imply the type + * from an artificial simple type. + */ + private final static Set> NATIVE_TIME_TYPE_MARKERS; + static { List converters = new ArrayList<>(); @@ -48,6 +66,23 @@ public class CassandraCustomConversions extends org.springframework.data.convert STORE_CONVERTERS = Collections.unmodifiableList(converters); STORE_CONVERSIONS = StoreConversions.of(CassandraSimpleTypeHolder.HOLDER, STORE_CONVERTERS); + + List> timeMarkers = STORE_CONVERTERS.stream() // + .filter(Converter.class::isInstance) // + .map(Object::getClass) // + .filter(it -> AnnotatedElementUtils.hasAnnotation(it, WritingConverter.class)) // + .filter(it -> { + + CassandraType annotation = AnnotatedElementUtils.getMergedAnnotation(it, CassandraType.class); + return annotation != null && annotation.type() == Name.TIME; + }) // + .map(it -> { + + ResolvableType classType = ResolvableType.forClass(it).as(Converter.class).getGeneric(0); + return classType.getRawClass(); + }).collect(Collectors.toList()); + + NATIVE_TIME_TYPE_MARKERS = new HashSet<>(timeMarkers); } /** @@ -58,4 +93,18 @@ public class CassandraCustomConversions extends org.springframework.data.convert public CassandraCustomConversions(List converters) { super(STORE_CONVERSIONS, converters); } + + /** + * Returns {@literal true} if the {@link Class type} is used to denote Cassandra's {@code time} column type. + * + * @param type must not be {@literal null}. + * @return {@literal true} if the type maps to Cassandra's {@code time} column type. + * @since 2.1 + */ + public boolean isNativeTimeTypeMarker(Class type) { + + Assert.notNull(type, "Type must not be null"); + + return NATIVE_TIME_TYPE_MARKERS.contains(ClassUtils.getUserClass(type)); + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJodaTimeConverters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJodaTimeConverters.java index fa85d0fc1..0286a1922 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJodaTimeConverters.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJodaTimeConverters.java @@ -21,9 +21,14 @@ import java.util.Collections; import java.util.List; import org.joda.time.LocalDate; +import org.joda.time.LocalTime; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.core.mapping.CassandraType; +import org.springframework.data.convert.WritingConverter; import org.springframework.util.ClassUtils; +import com.datastax.driver.core.DataType.Name; + /** * Helper class to register JSR-310 specific {@link Converter} implementations to convert between Cassandra types in * case the library is present on the classpath. @@ -53,6 +58,8 @@ public abstract class CassandraJodaTimeConverters { converters.add(CassandraLocalDateToLocalDateConverter.INSTANCE); converters.add(LocalDateToCassandraLocalDateConverter.INSTANCE); + converters.add(MillisOfDayToLocalTimeConverter.INSTANCE); + converters.add(LocalTimeToMillisOfDayConverter.INSTANCE); return converters; } @@ -89,4 +96,36 @@ public abstract class CassandraJodaTimeConverters { source.getDayOfMonth()); } } + + /** + * Simple singleton to convert {@link Long}s to their {@link LocalTime} representation. + * + * @author Mark Paluch + */ + public enum MillisOfDayToLocalTimeConverter implements Converter { + + INSTANCE; + + @Override + public LocalTime convert(Long source) { + return LocalTime.fromMillisOfDay(source); + } + } + + /** + * Simple singleton to convert {@link LocalTime}s to their {@link Long} representation. + * + * @author Mark Paluch + */ + @WritingConverter + @CassandraType(type = Name.TIME) + public enum LocalTimeToMillisOfDayConverter implements Converter { + + INSTANCE; + + @Override + public Long convert(LocalTime source) { + return (long) source.getMillisOfDay(); + } + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJsr310Converters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJsr310Converters.java index 4c640a951..72ead53d7 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJsr310Converters.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJsr310Converters.java @@ -16,11 +16,18 @@ package org.springframework.data.cassandra.core.convert; import java.time.LocalDate; +import java.time.LocalTime; +import java.time.temporal.ChronoField; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.TimeUnit; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.core.mapping.CassandraType; +import org.springframework.data.convert.WritingConverter; + +import com.datastax.driver.core.DataType.Name; /** * Helper class to register JodaTime specific {@link Converter} implementations in case the library is present on the @@ -44,6 +51,8 @@ public abstract class CassandraJsr310Converters { converters.add(CassandraLocalDateToLocalDateConverter.INSTANCE); converters.add(LocalDateToCassandraLocalDateConverter.INSTANCE); + converters.add(MillisOfDayToLocalTimeConverter.INSTANCE); + converters.add(LocalTimeToMillisOfDayConverter.INSTANCE); return converters; } @@ -80,4 +89,38 @@ public abstract class CassandraJsr310Converters { source.getDayOfMonth()); } } + + /** + * Simple singleton to convert {@link Long}s to their {@link LocalTime} representation. + * + * @author Mark Paluch + * @since 2.1 + */ + public enum MillisOfDayToLocalTimeConverter implements Converter { + + INSTANCE; + + @Override + public LocalTime convert(Long source) { + return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(source)); + } + } + + /** + * Simple singleton to convert {@link LocalTime}s to their {@link Long} representation. + * + * @author Mark Paluch + * @since 2.1 + */ + @WritingConverter + @CassandraType(type = Name.TIME) + public enum LocalTimeToMillisOfDayConverter implements Converter { + + INSTANCE; + + @Override + public Long convert(LocalTime source) { + return source.getLong(ChronoField.MILLI_OF_DAY); + } + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraThreeTenBackPortConverters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraThreeTenBackPortConverters.java index 1e81deee3..d44ff1b43 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraThreeTenBackPortConverters.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraThreeTenBackPortConverters.java @@ -19,11 +19,18 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.core.mapping.CassandraType; import org.springframework.data.convert.ThreeTenBackPortConverters; +import org.springframework.data.convert.WritingConverter; import org.springframework.util.ClassUtils; import org.threeten.bp.LocalDate; +import org.threeten.bp.LocalTime; +import org.threeten.bp.temporal.ChronoField; + +import com.datastax.driver.core.DataType.Name; /** * Helper class to register {@link Converter} implementations for the ThreeTen Backport project in case it's present on @@ -56,6 +63,8 @@ public abstract class CassandraThreeTenBackPortConverters { converters.add(CassandraLocalDateToLocalDateConverter.INSTANCE); converters.add(LocalDateToCassandraLocalDateConverter.INSTANCE); + converters.add(MillisOfDayToLocalTimeConverter.INSTANCE); + converters.add(LocalTimeToMillisOfDayConverter.INSTANCE); return converters; } @@ -92,4 +101,39 @@ public abstract class CassandraThreeTenBackPortConverters { source.getDayOfMonth()); } } + + /** + * Simple singleton to convert {@link Long}s to their {@link LocalTime} representation. + * + * @author Mark Paluch + * @since 2.1 + */ + + public enum MillisOfDayToLocalTimeConverter implements Converter { + + INSTANCE; + + @Override + public LocalTime convert(Long source) { + return LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(source)); + } + } + + /** + * Simple singleton to convert {@link LocalTime}s to their {@link Long} representation. + * + * @author Mark Paluch + * @since 2.1 + */ + @WritingConverter + @CassandraType(type = Name.TIME) + public enum LocalTimeToMillisOfDayConverter implements Converter { + + INSTANCE; + + @Override + public Long convert(LocalTime source) { + return source.getLong(ChronoField.MILLI_OF_DAY); + } + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraMappingContext.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraMappingContext.java index 9fcb9523e..808618d8a 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraMappingContext.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraMappingContext.java @@ -37,13 +37,13 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.cassandra.core.convert.CassandraCustomConversions; import org.springframework.data.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.core.cql.keyspace.CreateIndexSpecification; import org.springframework.data.cassandra.core.cql.keyspace.CreateTableSpecification; import org.springframework.data.cassandra.core.cql.keyspace.CreateUserTypeSpecification; import org.springframework.data.cassandra.core.mapping.UserTypeUtil.FrozenLiteralDataType; import org.springframework.data.convert.CustomConversions; -import org.springframework.data.convert.CustomConversions.StoreConversions; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.context.MappingContext; @@ -82,8 +82,7 @@ public class CassandraMappingContext private @Nullable ClassLoader beanClassLoader; - private CustomConversions customConversions = - new CustomConversions(StoreConversions.of(CassandraSimpleTypeHolder.HOLDER), Collections.emptyList()); + private CustomConversions customConversions = new CassandraCustomConversions(Collections.emptyList()); private Mapping mapping = new Mapping(); @@ -524,10 +523,7 @@ public class CassandraMappingContext * @since 1.5 */ public DataType getDataType(Class type) { - - return this.customConversions.getCustomWriteTarget(type) - .map(CassandraSimpleTypeHolder::getDataTypeFor) - .orElseGet(() -> getDataTypeFor(type)); + return doGetDataType(type, this.customConversions.getCustomWriteTarget(type).orElse(type)); } public TupleType getTupleType(CassandraPersistentEntity persistentEntity) { @@ -552,6 +548,7 @@ public class CassandraMappingContext return getDataTypeWithUserTypeFactory(property, DataTypeProvider.EntityUserType); } + @Nullable private DataType getDataTypeWithUserTypeFactory(CassandraPersistentProperty property, DataTypeProvider dataTypeProvider) { @@ -591,6 +588,7 @@ public class CassandraMappingContext return getDataTypeWithUserTypeFactory(property.getTypeInformation(), dataTypeProvider, property::getDataType); } + @Nullable private DataType getDataTypeWithUserTypeFactory(TypeInformation typeInformation, DataTypeProvider dataTypeProvider, Supplier fallback) { @@ -613,30 +611,56 @@ public class CassandraMappingContext } Optional customWriteTarget = this.customConversions.getCustomWriteTarget(typeInformation.getType()) - .map(CassandraSimpleTypeHolder::getDataTypeFor); + .map(it -> doGetDataType(typeInformation.getType(), it)); DataType dataType = customWriteTarget - .orElseGet(() -> this.customConversions.getCustomWriteTarget(typeInformation.getRequiredActualType().getType()) - .filter(it -> !typeInformation.isMap()).map(it -> { + .orElseGet(() -> { + Class propertyType = typeInformation.getRequiredActualType().getType(); + return this.customConversions.getCustomWriteTarget(propertyType).filter(it -> !typeInformation.isMap()) + .map(it -> { - if (typeInformation.isCollectionLike()) { - if (List.class.isAssignableFrom(typeInformation.getType())) { - return DataType.list(getDataTypeFor(it)); + if (typeInformation.isCollectionLike()) { + if (List.class.isAssignableFrom(typeInformation.getType())) { + return DataType.list(doGetDataType(propertyType, it)); + } + + if (Set.class.isAssignableFrom(typeInformation.getType())) { + return DataType.set(doGetDataType(propertyType, it)); + } } - if (Set.class.isAssignableFrom(typeInformation.getType())) { - return DataType.set(getDataTypeFor(it)); - } - } + return doGetDataType(propertyType, it); - return getDataTypeFor(it); - - }).orElse(null)); + }).orElse(null); + }); return dataType != null ? dataType : typeInformation.isMap() ? getMapDataType(typeInformation, dataTypeProvider) : fallback.get(); } + /** + * Resolve Cassandra {@link DataType} with conditional handling of {@code time} data type. + * + * @param propertyType + * @param converted + * @return + */ + @Nullable + private DataType doGetDataType(Class propertyType, Class converted) { + + if (customConversions instanceof CassandraCustomConversions) { + + CassandraCustomConversions conversions = (CassandraCustomConversions) customConversions; + + if (conversions.isNativeTimeTypeMarker(converted) + || (conversions.isNativeTimeTypeMarker(propertyType) && Long.class.equals(converted))) { + return DataType.time(); + } + } + + return CassandraSimpleTypeHolder.getDataTypeFor(converted); + } + private TupleType getTupleType(DataTypeProvider dataTypeProvider, CassandraPersistentEntity persistentEntity) { List types = new ArrayList<>(); @@ -656,7 +680,7 @@ public class CassandraMappingContext DataType keyType = getDataTypeWithUserTypeFactory(keyTypeInformation, dataTypeProvider, () -> { - DataType type = getDataTypeFor(keyTypeInformation.getType()); + DataType type = doGetDataType(keyTypeInformation.getType(), keyTypeInformation.getType()); if (type != null) { return type; @@ -667,7 +691,7 @@ public class CassandraMappingContext DataType valueType = getDataTypeWithUserTypeFactory(valueTypeInformation, dataTypeProvider, () -> { - DataType type = getDataTypeFor(valueTypeInformation.getType()); + DataType type = doGetDataType(valueTypeInformation.getType(), valueTypeInformation.getType()); if (type != null) { return type; @@ -680,7 +704,7 @@ public class CassandraMappingContext } @Nullable - private DataType getUserDataType(TypeInformation property, DataType elementType) { + private DataType getUserDataType(TypeInformation property, @Nullable DataType elementType) { if (property.isCollectionLike()) { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraSimpleTypeHolder.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraSimpleTypeHolder.java index 30f7ef308..4d90bba23 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraSimpleTypeHolder.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/mapping/CassandraSimpleTypeHolder.java @@ -15,6 +15,7 @@ */ package org.springframework.data.cassandra.core.mapping; +import java.time.LocalTime; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -121,6 +122,9 @@ public class CassandraSimpleTypeHolder extends SimpleTypeHolder { // override UUID to timeuuid as regular uuid as the favored default classToDataType.put(UUID.class, DataType.uuid()); + // override LocalTime to time as time columns are mapped to long by default + classToDataType.put(LocalTime.class, DataType.time()); + return classToDataType; } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java index f7b058915..4a16bbf55 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateIntegrationTests.java @@ -19,8 +19,11 @@ import static org.assertj.core.api.Assertions.*; import static org.junit.Assume.*; import static org.springframework.data.cassandra.core.query.Criteria.*; +import lombok.Data; + import java.time.Instant; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.ZoneId; import java.util.Arrays; import java.util.Collections; @@ -33,6 +36,7 @@ import java.util.stream.Stream; import org.junit.Before; import org.junit.Test; +import org.springframework.data.annotation.Id; import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.cql.CqlTemplate; import org.springframework.data.cassandra.core.mapping.BasicMapId; @@ -78,6 +82,7 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI SchemaTestUtils.potentiallyCreateTableFor(User.class, template); SchemaTestUtils.potentiallyCreateTableFor(UserToken.class, template); SchemaTestUtils.potentiallyCreateTableFor(BookReference.class, template); + SchemaTestUtils.potentiallyCreateTableFor(TimeClass.class, template); SchemaTestUtils.truncate(User.class, template); SchemaTestUtils.truncate(UserToken.class, template); SchemaTestUtils.truncate(BookReference.class, template); @@ -478,4 +483,11 @@ public class CassandraTemplateIntegrationTests extends AbstractKeyspaceCreatingI assertThat(ids).containsAll(expectedIds); assertThat(iterations).isEqualTo(10); } + + @Data + static class TimeClass { + + @Id LocalTime id; + LocalTime bar; + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraJodaTimeConvertersUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraJodaTimeConvertersUnitTests.java new file mode 100644 index 000000000..8f3da6419 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraJodaTimeConvertersUnitTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core.convert; + +import static org.assertj.core.api.Assertions.*; + +import org.joda.time.LocalTime; +import org.junit.Test; +import org.springframework.data.cassandra.core.convert.CassandraJodaTimeConverters.LocalTimeToMillisOfDayConverter; +import org.springframework.data.cassandra.core.convert.CassandraJodaTimeConverters.MillisOfDayToLocalTimeConverter; + +/** + * Unit tests for {@link CassandraJodaTimeConverters}. + * + * @author Mark Paluch + */ +public class CassandraJodaTimeConvertersUnitTests { + + @Test // DATACASS-302 + public void shouldConvertLocalTimeToLong() { + + assertThat(MillisOfDayToLocalTimeConverter.INSTANCE.convert(3723000L)) + .isEqualTo(LocalTime.fromMillisOfDay(3723000L)); + } + + @Test // DATACASS-302 + public void shouldConvertLongToLocalTime() { + + assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.MIDNIGHT)).isZero(); + assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.fromMillisOfDay(3723000L))) + .isEqualTo(3723000L); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraJsr310ConvertersUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraJsr310ConvertersUnitTests.java new file mode 100644 index 000000000..3a484c343 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraJsr310ConvertersUnitTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core.convert; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalTime; + +import org.junit.Test; +import org.springframework.data.cassandra.core.convert.CassandraJsr310Converters.LocalTimeToMillisOfDayConverter; +import org.springframework.data.cassandra.core.convert.CassandraJsr310Converters.MillisOfDayToLocalTimeConverter; + +/** + * Unit tests for {@link CassandraJsr310Converters}. + * + * @author Mark Paluch + */ +public class CassandraJsr310ConvertersUnitTests { + + @Test // DATACASS-302 + public void shouldConvertLocalTimeToLong() { + + assertThat(MillisOfDayToLocalTimeConverter.INSTANCE.convert(3723000L)).isEqualTo(LocalTime.of(1, 2, 3)); + } + + @Test // DATACASS-302 + public void shouldConvertLongToLocalTime() { + + assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.MIDNIGHT)).isZero(); + assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.of(1, 2, 3))).isEqualTo(3723000L); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraThreeTenBackPortConvertersUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraThreeTenBackPortConvertersUnitTests.java new file mode 100644 index 000000000..3e48c6718 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraThreeTenBackPortConvertersUnitTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.core.convert; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.springframework.data.cassandra.core.convert.CassandraThreeTenBackPortConverters.LocalTimeToMillisOfDayConverter; +import org.springframework.data.cassandra.core.convert.CassandraThreeTenBackPortConverters.MillisOfDayToLocalTimeConverter; +import org.threeten.bp.LocalTime; + +/** + * Unit tests for {@link CassandraThreeTenBackPortConverters}. + * + * @author Mark Paluch + */ +public class CassandraThreeTenBackPortConvertersUnitTests { + + @Test // DATACASS-302 + public void shouldConvertLocalTimeToLong() { + + assertThat(MillisOfDayToLocalTimeConverter.INSTANCE.convert(3723000L)).isEqualTo(LocalTime.of(1, 2, 3)); + } + + @Test // DATACASS-302 + public void shouldConvertLongToLocalTime() { + + assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.MIDNIGHT)).isZero(); + assertThat(LocalTimeToMillisOfDayConverter.INSTANCE.convert(LocalTime.of(1, 2, 3))).isEqualTo(3723000L); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraTypeMappingIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraTypeMappingIntegrationTests.java index 3b55f2d75..5fe6cc765 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraTypeMappingIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/CassandraTypeMappingIntegrationTests.java @@ -35,10 +35,10 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; - import org.springframework.data.annotation.Id; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.CassandraTemplate; @@ -48,8 +48,6 @@ import org.springframework.data.cassandra.support.CassandraVersion; import org.springframework.data.cassandra.test.util.AbstractKeyspaceCreatingIntegrationTest; import org.springframework.data.util.Version; -import org.assertj.core.api.Assertions; - import com.datastax.driver.core.DataType; import com.datastax.driver.core.Duration; import com.datastax.driver.core.LocalDate; @@ -431,7 +429,7 @@ public class CassandraTypeMappingIntegrationTests extends AbstractKeyspaceCreati operations.insert(entity); AllPossibleTypes loaded = load(entity); - Assertions.assertThat(loaded.getAnEnum()).isEqualTo(entity.getAnEnum()); + assertThat(loaded.getAnEnum()).isEqualTo(entity.getAnEnum()); } @Test // DATACASS-280 @@ -545,6 +543,18 @@ public class CassandraTypeMappingIntegrationTests extends AbstractKeyspaceCreati assertThat(loaded.getLocalTime()).isEqualTo(entity.getLocalTime()); } + @Test // DATACASS-296 + public void shouldReadAndWriteJodaLocalTime() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setJodaLocalTime(org.joda.time.LocalTime.fromMillisOfDay(50000)); + + operations.insert(entity); + AllPossibleTypes loaded = load(entity); + + assertThat(loaded.getJodaLocalTime()).isEqualTo(entity.getJodaLocalTime()); + } + @Test // DATACASS-296 public void shouldReadAndWriteInstant() { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/QueryMapperUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/QueryMapperUnitTests.java index 258c69962..e2a00083a 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/QueryMapperUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/QueryMapperUnitTests.java @@ -27,13 +27,13 @@ import java.util.Map; import java.util.stream.Collectors; import lombok.AllArgsConstructor; - +import org.joda.time.LocalDate; +import org.joda.time.LocalTime; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.data.annotation.Id; import org.springframework.data.cassandra.core.cql.CqlIdentifier; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; @@ -348,6 +348,17 @@ public class QueryMapperUnitTests { assertThat(mappedObject).contains(Criteria.where("tuple").is(tupleValue)); } + @Test // DATACASS-302 + public void shouldMapTime() { + + Filter filter = Filter.from(Criteria.where("localDate").gt(LocalTime.fromMillisOfDay(1000))); + + Filter mappedObject = this.queryMapper.getMappedObject(filter, + this.mappingContext.getRequiredPersistentEntity(Person.class)); + + assertThat(mappedObject).contains(Criteria.where("localdate").gt(1000L)); + } + @Test(expected = IllegalArgumentException.class) // DATACASS-523 public void referencingTupleElementsInQueryShouldFail() { @@ -367,6 +378,8 @@ public class QueryMapperUnitTests { Integer number; + LocalDate localDate; + MappedTuple tuple; @Column("first_name") String firstName; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/UpdateMapperUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/UpdateMapperUnitTests.java index af3306d68..d4eb2bf63 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/UpdateMapperUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/convert/UpdateMapperUnitTests.java @@ -18,6 +18,7 @@ package org.springframework.data.cassandra.core.convert; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; +import java.time.LocalTime; import java.util.Collections; import java.util.Currency; import java.util.List; @@ -238,6 +239,16 @@ public class UpdateMapperUnitTests { assertThat(update.toString()).isEqualTo("tuple = ('foo')"); } + @Test // DATACASS-302 + public void shouldMapTime() { + + Update update = this.updateMapper.getMappedObject(Update.empty().set("localTime", LocalTime.of(1, 2, 3)), + this.persistentEntity); + + assertThat(update.getUpdateOperations()).hasSize(1); + assertThat(update.toString()).isEqualTo("localtime = 3723000"); + } + @Test(expected = IllegalArgumentException.class) // DATACASS-523 public void referencingTupleElementsInQueryShouldFail() { this.updateMapper.getMappedObject(Update.empty().set("tuple.zip", "bar"), this.persistentEntity); @@ -252,6 +263,7 @@ public class UpdateMapperUnitTests { Map map; Map manufacturers; Currency currency; + LocalTime localTime; Integer number; MappedTuple tuple; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/CassandraMappingContextUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/CassandraMappingContextUnitTests.java index 2b98edfa4..c32be4bc2 100755 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/CassandraMappingContextUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/CassandraMappingContextUnitTests.java @@ -29,7 +29,6 @@ import java.util.NoSuchElementException; import org.junit.Before; import org.junit.Test; - import org.springframework.core.convert.converter.Converter; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; @@ -41,6 +40,7 @@ import org.springframework.data.cassandra.core.cql.keyspace.ColumnSpecification; import org.springframework.data.cassandra.core.cql.keyspace.CreateIndexSpecification; import org.springframework.data.cassandra.core.cql.keyspace.CreateIndexSpecification.ColumnFunction; import org.springframework.data.cassandra.core.cql.keyspace.CreateTableSpecification; +import org.springframework.data.cassandra.domain.AllPossibleTypes; import org.springframework.data.cassandra.support.UserTypeBuilder; import org.springframework.data.convert.WritingConverter; import org.springframework.data.mapping.MappingException; @@ -463,6 +463,17 @@ public class CassandraMappingContextUnitTests { .isEqualTo(DataType.list(DataType.varchar())); } + @Test // DATACASS-302 + public void propertyTypeShouldMapToTime() { + + CassandraPersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(AllPossibleTypes.class); + + assertThat(mappingContext.getDataType(persistentEntity.getRequiredPersistentProperty("localTime"))) + .isEqualTo(DataType.time()); + assertThat(mappingContext.getDataType(persistentEntity.getRequiredPersistentProperty("jodaLocalTime"))) + .isEqualTo(DataType.time()); + } + @Test // DATACASS-172, DATACASS-455 public void shouldRegisterUdtTypes() { 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 7cb620d11..544f888ae 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 @@ -104,6 +104,7 @@ public class AllPossibleTypes { org.joda.time.DateTime jodaDateTime; org.joda.time.LocalDate jodaLocalDate; org.joda.time.LocalDateTime jodaLocalDateTime; + org.joda.time.LocalTime jodaLocalTime; org.threeten.bp.Instant bpInstant; org.threeten.bp.LocalDate bpLocalDate; diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index ba2cf3bc7..8d06eecc7 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -9,6 +9,7 @@ This chapter summarizes changes and new features for each release. * Template API extended with `count(…)` and `exists(…)` methods accepting `Query`. * <> for CRUD operations. * Cassandra Mapped Tuple support via `@Tuple`. +* Support for Cassandra `time` columns via `LocalTime`. * Support for `map` columns using User-defined/converted types. * <> diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index ca084add1..5b12b078e 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -95,6 +95,10 @@ for further details. (Joda, Java 8, JSR310-BackPort) | `date` +| `LocalTime`+ +(Joda, Java 8, JSR310-BackPort) +| `time` + | `LocalDateTime`, `LocalTime`, `Instant` + (Joda, Java 8, JSR310-BackPort) | `timestamp`