From 4772c5b6289e63fbc050e5a04eeaf6081ba66f98 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 27 Apr 2016 15:27:39 +0200 Subject: [PATCH] DATACASS-280 - Refactor Cassandra query execution and mapping to consolidate mapping. We now support Optional provided by Spring Data Commons on Repository methods carrying the appropriate element type. This commit refactors the mapping layer for Cassandra query methods with following changes: Pull mapping from AbstractCassandraQuery into AbstractCassandraConverter. Throw UnsupportedOperationException in deprecated AbstractCassandraQuery.setConversionService method. Refactor conditional execution to CassandraQueryExecution pattern and follow CassandraConverters/CustomConversions pattern. Introduce DtoInstantiatingConverter and Reading/Writing converters. Refactor lookup map creation to multiple methods. Rename fields. Remove unused primitiveTypesByWrapperType. Fix error message of getDataTypeNamesFrom method. Add JavaDoc and extend not-null assertions with a meaningful message. Remove Java 7 build profile from TravisCI as these changes require the usage of Java 8 within the tests. Original pull request: #53 Related ticket: DATACASS-247 --- .travis.yml | 1 - .../core/converter/RowToListConverter.java | 33 +- .../core/converter/RowToMapConverter.java | 33 +- .../cassandra/support/CassandraAccessor.java | 67 ++- .../convert/AbstractCassandraConverter.java | 48 +- .../convert/CassandraConverters.java | 187 +++++++ .../convert/ConverterRegistration.java | 116 ++++ .../cassandra/convert/CustomConversions.java | 372 +++++++++++++ .../convert/MappingCassandraConverter.java | 26 +- .../cassandra/core/CassandraTemplate.java | 45 +- .../mapping/BasicCassandraMappingContext.java | 10 +- ...andraPersistentEntityMetadataVerifier.java | 10 +- .../mapping/CassandraSimpleTypeHolder.java | 117 ++-- .../query/AbstractCassandraQuery.java | 139 +++-- .../query/CassandraQueryExecution.java | 125 +++++ .../ConverterRegistrationUnitTests.java | 79 +++ .../convert/CustomConversionsUnitTests.java | 306 +++++++++++ .../MappingCassandraConverterUnitTests.java | 190 ++++++- .../CassandraOperationsIntegrationTests.java | 16 +- .../CassandraTypeMappingIntegrationTest.java | 509 ++++++++++++++++++ .../querymethods/declared/Person.java | 64 +-- .../declared/QueryIntegrationTests.java | 25 + .../PersonRepositoryWithQueryAnnotations.java | 5 + .../declared/base/PersonRepository.java | 4 + ...ersonRepositoryWithNamedQueries.properties | 2 + spring-data-cassandra/template.mf | 4 +- 26 files changed, 2287 insertions(+), 246 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverters.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ConverterRegistration.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/ConverterRegistrationUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CustomConversionsUnitTests.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/types/CassandraTypeMappingIntegrationTest.java diff --git a/.travis.yml b/.travis.yml index 3a0a7bb9e..9a0980bde 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: java jdk: - - oraclejdk7 - oraclejdk8 env: matrix: diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToListConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToListConverter.java index 2a06d0a5b..4fcf48174 100644 --- a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToListConverter.java +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToListConverter.java @@ -1,17 +1,44 @@ +/* + * 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.cassandra.core.converter; import java.util.ArrayList; import java.util.List; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; import com.datastax.driver.core.ColumnDefinitions; +import com.datastax.driver.core.ColumnDefinitions.Definition; import com.datastax.driver.core.ProtocolVersion; import com.datastax.driver.core.Row; -import com.datastax.driver.core.ColumnDefinitions.Definition; +/** + * Converter to convert {@link Row}s to a {@link List} of {@link Object} representation. + * + * @author Matthew T. Adams + * @author Stefan Birkner + * @author Mark Paluch + */ +@ReadingConverter public class RowToListConverter implements Converter> { + public final static RowToListConverter INSTANCE = new RowToListConverter(); + @Override public List convert(Row row) { @@ -24,8 +51,8 @@ public class RowToListConverter implements Converter> { for (Definition def : cols.asList()) { String name = def.getName(); - list.add(row.isNull(name) ? null : def.getType().deserialize( - row.getBytesUnsafe(name), ProtocolVersion.NEWEST_SUPPORTED)); + list.add(row.isNull(name) ? null + : def.getType().deserialize(row.getBytesUnsafe(name), ProtocolVersion.NEWEST_SUPPORTED)); } return list; diff --git a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToMapConverter.java b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToMapConverter.java index b588099f3..6e3474c66 100644 --- a/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToMapConverter.java +++ b/spring-cql/src/main/java/org/springframework/cassandra/core/converter/RowToMapConverter.java @@ -1,17 +1,44 @@ +/* + * 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.cassandra.core.converter; import java.util.HashMap; import java.util.Map; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; import com.datastax.driver.core.ColumnDefinitions; import com.datastax.driver.core.ColumnDefinitions.Definition; import com.datastax.driver.core.ProtocolVersion; import com.datastax.driver.core.Row; +/** + * Converter to convert {@link Row}s to a {@link Map} of {@link String}/{@link Object} representation. + * + * @author Matthew T. Adams + * @author David Webb + * @author Mark Paluch + */ +@ReadingConverter public class RowToMapConverter implements Converter> { + public final static RowToMapConverter INSTANCE = new RowToMapConverter(); + @Override public Map convert(Row row) { @@ -25,10 +52,8 @@ public class RowToMapConverter implements Converter> { for (Definition def : cols.asList()) { String name = def.getName(); - map.put( - name, - row.isNull(name) ? null : def.getType().deserialize(row.getBytesUnsafe(name), - ProtocolVersion.NEWEST_SUPPORTED)); + map.put(name, row.isNull(name) ? null + : def.getType().deserialize(row.getBytesUnsafe(name), ProtocolVersion.NEWEST_SUPPORTED)); } return map; diff --git a/spring-cql/src/main/java/org/springframework/cassandra/support/CassandraAccessor.java b/spring-cql/src/main/java/org/springframework/cassandra/support/CassandraAccessor.java index 450d1e078..d96f1f8de 100644 --- a/spring-cql/src/main/java/org/springframework/cassandra/support/CassandraAccessor.java +++ b/spring-cql/src/main/java/org/springframework/cassandra/support/CassandraAccessor.java @@ -1,12 +1,12 @@ /* - * 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. * 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. @@ -23,7 +23,15 @@ import org.springframework.util.Assert; import com.datastax.driver.core.Session; /** + * A {@link CassandraAccessor} is able to access a Cassandra {@link Session} and the + * {@link CassandraExceptionTranslator}. + *

+ * Classes providing a higher abstraction level usually extend {@link CassandraAccessor} to provide a richer set of + * functionality on top of a {@link Session}. + * * @author David Webb + * @author Mark Paluch + * @see org.springframework.beans.factory.InitializingBean */ public class CassandraAccessor implements InitializingBean { @@ -31,45 +39,54 @@ public class CassandraAccessor implements InitializingBean { protected final Logger logger = LoggerFactory.getLogger(getClass()); private Session session; - private CassandraExceptionTranslator exceptionTranslator = new CassandraExceptionTranslator(); - /** - * Set the exception translator for this instance. - * - * @see org.springframework.cassandra.support.CassandraExceptionTranslator - */ - public void setExceptionTranslator(CassandraExceptionTranslator exceptionTranslator) { - this.exceptionTranslator = exceptionTranslator; - } - - /** - * Return the exception translator for this instance. - */ - public CassandraExceptionTranslator getExceptionTranslator() { - return this.exceptionTranslator; - } - /** * Ensure that the Cassandra Session has been set */ @Override public void afterPropertiesSet() { - Assert.notNull(session); + + Assert.notNull(session, "Session must not be null!"); + Assert.notNull(exceptionTranslator, "CassandraExceptionTranslator must not be null!"); } /** - * @return Returns the session. + * Set the exception translator for this instance. + * + * @param exceptionTranslator the exception translator to set, must not be {@literal null}. + * @see org.springframework.cassandra.support.CassandraExceptionTranslator + */ + public void setExceptionTranslator(CassandraExceptionTranslator exceptionTranslator) { + + Assert.notNull(exceptionTranslator, "CassandraExceptionTranslator must not be null!"); + this.exceptionTranslator = exceptionTranslator; + } + + /** + * Return the exception translator for this instance. + * + * @return the exception translator + */ + public CassandraExceptionTranslator getExceptionTranslator() { + return this.exceptionTranslator; + } + + /** + * Returns the session. + * + * @return the session. */ public Session getSession() { return session; } /** - * @param session The session to set. + * @param session The session to set, must not be{@literal null} */ public void setSession(Session session) { - Assert.notNull(session); + + Assert.notNull(session, "Session must not be null!"); this.session = session; } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java index bbc9402d9..296b33055 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/AbstractCassandraConverter.java @@ -1,12 +1,12 @@ /* - * 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. * 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. @@ -18,18 +18,23 @@ package org.springframework.data.cassandra.convert; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.convert.EntityInstantiators; /** * Base class for {@link CassandraConverter} implementations. Sets up a {@link ConversionService} and populates basic * converters. - * + * * @author Alex Shvid * @author Matthew T. Adams + * @author Mark Paluch + * @see org.springframework.beans.factory.InitializingBean + * @see org.springframework.data.cassandra.convert.CassandraConverter */ public abstract class AbstractCassandraConverter implements CassandraConverter, InitializingBean { protected final ConversionService conversionService; + protected CustomConversions conversions = new CustomConversions(); protected EntityInstantiators instantiators = new EntityInstantiators(); /** @@ -41,18 +46,43 @@ public abstract class AbstractCassandraConverter implements CassandraConverter, /** * Registers {@link EntityInstantiators} to customize entity instantiation. - * + * * @param instantiators */ public void setInstantiators(EntityInstantiators instantiators) { this.instantiators = instantiators == null ? new EntityInstantiators() : instantiators; } + /** + * Registers the given custom conversions with the converter. + * + * @param conversions + */ + public void setCustomConversions(CustomConversions conversions) { + this.conversions = conversions; + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + initializeConverters(); + } + + /** + * Registers additional converters that will be available when using the {@link ConversionService} directly (e.g. for + * id conversion). These converters are not custom conversions as they'd introduce unwanted conversions. + */ + private void initializeConverters() { + + if (conversionService instanceof GenericConversionService) { + conversions.registerConvertersIn((GenericConversionService) conversionService); + } + } + @Override public ConversionService getConversionService() { return conversionService; } - - @Override - public void afterPropertiesSet() {} } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverters.java new file mode 100644 index 000000000..ad7628871 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CassandraConverters.java @@ -0,0 +1,187 @@ +/* + * 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.net.InetAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.springframework.cassandra.core.converter.RowToListConverter; +import org.springframework.cassandra.core.converter.RowToMapConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +import com.datastax.driver.core.Row; + +/** + * Wrapper class to contain useful converters for the usage with Cassandra. + * + * @author Mark Paluch + * @since 1.5 + */ +abstract class CassandraConverters { + + /** + * Private constructor to prevent instantiation. + */ + private CassandraConverters() {} + + /** + * Returns the converters to be registered. + * + * @return + */ + public static Collection getConvertersToRegister() { + + List converters = new ArrayList(); + + converters.add(RowToNumberConverterFactory.INSTANCE); + converters.add(RowToBooleanConverter.INSTANCE); + converters.add(RowToDateConverter.INSTANCE); + converters.add(RowToInetAddressConverter.INSTANCE); + converters.add(RowToStringConverter.INSTANCE); + converters.add(RowToUuidConverter.INSTANCE); + converters.add(RowToListConverter.INSTANCE); + converters.add(RowToMapConverter.INSTANCE); + + return converters; + } + + @ReadingConverter + public enum RowToBooleanConverter implements Converter { + INSTANCE; + + @Override + public Boolean convert(Row row) { + return row.getBool(0); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link Date} representation. + * + * @author Mark Paluch + */ + @ReadingConverter + public enum RowToDateConverter implements Converter { + INSTANCE; + + @Override + public Date convert(Row row) { + + return row.getDate(0); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link InetAddress} representation. + * + * @author Mark Paluch + */ + @ReadingConverter + public enum RowToInetAddressConverter implements Converter { + INSTANCE; + + @Override + public InetAddress convert(Row row) { + return row.getInet(0); + } + } + + /** + * Singleton converter factory to convert the first column of a {@link Row} to a {@link Number}. + *

+ * Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. + * + * @see Byte + * @see Short + * @see Integer + * @see Long + * @see java.math.BigInteger + * @see Float + * @see Double + * @see java.math.BigDecimal + */ + @ReadingConverter + public enum RowToNumberConverterFactory implements ConverterFactory { + INSTANCE; + + @Override + public Converter getConverter(Class targetType) { + + Assert.notNull(targetType, "Target type must not be null"); + return new RowToNumber(targetType); + } + + private static final class RowToNumber implements Converter { + + private final Class targetType; + + public RowToNumber(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(Row source) { + + Object object = source.getObject(0); + if (object == null) { + return null; + } + + return NumberUtils.convertNumberToTargetClass((Number) object, this.targetType); + } + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link String} representation. + * + * @author Mark Paluch + */ + @ReadingConverter + public enum RowToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(Row row) { + return row.getString(0); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link UUID} representation. + * + * @author Mark Paluch + */ + @ReadingConverter + public enum RowToUuidConverter implements Converter { + INSTANCE; + + @Override + public UUID convert(Row row) { + return row.getUUID(0); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ConverterRegistration.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ConverterRegistration.java new file mode 100644 index 000000000..16b6df9b3 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/ConverterRegistration.java @@ -0,0 +1,116 @@ +/* + * 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 org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder; +import org.springframework.util.Assert; + +/** + * Conversion registration information. + * + * @author Mark Paluch + * @since 1.5 + */ +class ConverterRegistration { + + private final ConvertiblePair convertiblePair; + private final boolean reading; + private final boolean writing; + + /** + * Creates a new {@link ConverterRegistration}. + * + * @param convertiblePair must not be {@literal null}. + * @param isReading whether to force to consider the converter for reading. + * @param isWriting whether to force to consider the converter for reading. + */ + public ConverterRegistration(ConvertiblePair convertiblePair, boolean isReading, boolean isWriting) { + + Assert.notNull(convertiblePair); + + this.convertiblePair = convertiblePair; + this.reading = isReading; + this.writing = isWriting; + } + + /** + * Creates a new {@link ConverterRegistration} from the given source and target type and read/write flags. + * + * @param source the source type to be converted from, must not be {@literal null}. + * @param target the target type to be converted to, must not be {@literal null}. + * @param isReading whether to force to consider the converter for reading. + * @param isWriting whether to force to consider the converter for writing. + */ + public ConverterRegistration(Class source, Class target, boolean isReading, boolean isWriting) { + this(new ConvertiblePair(source, target), isReading, isWriting); + } + + /** + * Returns whether the converter shall be used for writing. + * + * @return + */ + public boolean isWriting() { + return writing || (!reading && isSimpleTargetType()); + } + + /** + * Returns whether the converter shall be used for reading. + * + * @return + */ + public boolean isReading() { + return reading || (!writing && isSimpleSourceType()); + } + + /** + * Returns the actual conversion pair. + * + * @return + */ + public ConvertiblePair getConvertiblePair() { + return convertiblePair; + } + + /** + * Returns whether the source type is a Cassandra simple one. + * + * @return + */ + public boolean isSimpleSourceType() { + return isCassandraBasicType(convertiblePair.getSourceType()); + } + + /** + * Returns whether the target type is a Cassandra simple one. + * + * @return + */ + public boolean isSimpleTargetType() { + return isCassandraBasicType(convertiblePair.getTargetType()); + } + + /** + * Returns whether the given type is a type that Cassandra can handle basically. + * + * @param type + * @return + */ + private static boolean isCassandraBasicType(Class type) { + return CassandraSimpleTypeHolder.HOLDER.isSimpleType(type); + } +} 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 new file mode 100644 index 000000000..f5a893acc --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/convert/CustomConversions.java @@ -0,0 +1,372 @@ +/* + * 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.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +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.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.CacheValue; +import org.springframework.util.Assert; + +/** + * Value object to capture custom conversion. That is essentially a {@link List} of converters and some additional logic + * around them. The converters are pretty much builds up two sets of types which Cassandra basic types can be converted + * into and from. These types will be considered simple ones (which means they neither need deeper inspection nor nested + * conversion. Thus the {@link CustomConversions} also act as factory for {@link SimpleTypeHolder} + * + * @author Mark Paluch + * @since 1.5 + */ +public class CustomConversions { + + private static final Logger LOG = LoggerFactory.getLogger(CustomConversions.class); + private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a Cassandra supported type! You might wanna check you annotation setup at the converter implementation."; + private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a Cassandra supported type! You might wanna check you annotation setup at the converter implementation."; + + private final Set readingPairs; + private final Set writingPairs; + private final Set> customSimpleTypes; + private final SimpleTypeHolder simpleTypeHolder; + + private final List converters; + + private final Map>> customReadTargetTypes; + private final Map>> customWriteTargetTypes; + private final Map, CacheValue>> rawWriteTargetTypes; + + /** + * Creates an empty {@link CustomConversions} object. + */ + CustomConversions() { + this(new ArrayList()); + } + + /** + * Creates a new {@link CustomConversions} instance registering the given converters. + * + * @param converters + */ + public CustomConversions(List converters) { + + Assert.notNull(converters); + + this.readingPairs = new LinkedHashSet(); + this.writingPairs = new LinkedHashSet(); + this.customSimpleTypes = new HashSet>(); + this.customReadTargetTypes = new ConcurrentHashMap>>(); + this.customWriteTargetTypes = new ConcurrentHashMap>>(); + this.rawWriteTargetTypes = new ConcurrentHashMap, CacheValue>>(); + + List toRegister = new ArrayList(); + + // Add user provided converters to make sure they can override the defaults + toRegister.addAll(converters); + toRegister.addAll(CassandraConverters.getConvertersToRegister()); + + for (Object c : toRegister) { + registerConversion(c); + } + + Collections.reverse(toRegister); + + this.converters = Collections.unmodifiableList(toRegister); + this.simpleTypeHolder = new CassandraSimpleTypeHolder(); + } + + /** + * 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. + * + * @see SimpleTypeHolder#isSimpleType(Class) + * @param type + * @return + */ + public boolean isSimpleType(Class type) { + return simpleTypeHolder.isSimpleType(type); + } + + /** + * Populates the given {@link GenericConversionService} with the registered converters. + * + * @param conversionService + */ + public void registerConvertersIn(GenericConversionService conversionService) { + + for (Object converter : converters) { + + boolean added = false; + + if (converter instanceof Converter) { + conversionService.addConverter((Converter) converter); + added = true; + } + + if (converter instanceof ConverterFactory) { + conversionService.addConverterFactory((ConverterFactory) converter); + added = true; + } + + if (converter instanceof GenericConverter) { + conversionService.addConverter((GenericConverter) converter); + added = true; + } + + if (!added) { + throw new IllegalArgumentException( + "Given set contains element that is neither Converter nor ConverterFactory!"); + } + } + } + + /** + * Registers a conversion for the given converter. Inspects either generics of {@link Converter} and + * {@link ConverterFactory} or the {@link ConvertiblePair}s returned by a {@link GenericConverter}. + * + * @param converter + */ + private void registerConversion(Object converter) { + + Class type = converter.getClass(); + boolean isWriting = type.isAnnotationPresent(WritingConverter.class); + boolean isReading = type.isAnnotationPresent(ReadingConverter.class); + + if (converter instanceof GenericConverter) { + GenericConverter genericConverter = (GenericConverter) converter; + for (ConvertiblePair pair : genericConverter.getConvertibleTypes()) { + register(new ConverterRegistration(pair, isReading, isWriting)); + } + } else if (converter instanceof ConverterFactory) { + + Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), ConverterFactory.class); + register(new ConverterRegistration(arguments[0], arguments[1], isReading, isWriting)); + } else if (converter instanceof Converter) { + Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), Converter.class); + register(new ConverterRegistration(arguments[0], arguments[1], isReading, isWriting)); + } else { + throw new IllegalArgumentException("Unsupported Converter type!"); + } + } + + /** + * Registers the given {@link ConvertiblePair} as reading or writing pair depending on the type sides being basic + * Cassandra types. + * + * @param converterRegistration + */ + private void register(ConverterRegistration converterRegistration) { + + ConvertiblePair pair = converterRegistration.getConvertiblePair(); + + if (converterRegistration.isReading()) { + + readingPairs.add(pair); + + if (LOG.isWarnEnabled() && !converterRegistration.isSimpleSourceType()) { + LOG.warn(String.format(READ_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType())); + } + } + + if (converterRegistration.isWriting()) { + + writingPairs.add(pair); + customSimpleTypes.add(pair.getSourceType()); + + if (LOG.isWarnEnabled() && !converterRegistration.isSimpleTargetType()) { + LOG.warn(String.format(WRITE_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType())); + } + } + } + + /** + * Returns the target type to convert to in case we have a custom conversion registered to convert the given source + * type into a Cassandra native one. + * + * @param sourceType must not be {@literal null} + * @return + */ + public Class getCustomWriteTarget(final Class sourceType) { + + return getOrCreateAndCache(sourceType, rawWriteTargetTypes, new Producer() { + + @Override + public Class get() { + return getCustomTarget(sourceType, null, writingPairs); + } + }); + } + + /** + * Returns the target type we can inject of the given source type to. The returned type might + * be a subclass of the given expected type though. If {@code expectedTargetType} is {@literal null} we will simply + * return the first target type matching or {@literal null} if no conversion can be found. + * + * @param sourceType must not be {@literal null} + * @param requestedTargetType + * @return + */ + public Class getCustomWriteTarget(final Class sourceType, final Class requestedTargetType) { + + if (requestedTargetType == null) { + return getCustomWriteTarget(sourceType); + } + + return getOrCreateAndCache(new ConvertiblePair(sourceType, requestedTargetType), customWriteTargetTypes, + new Producer() { + + @Override + public Class get() { + return getCustomTarget(sourceType, requestedTargetType, writingPairs); + } + }); + } + + /** + * Returns whether we have a custom conversion registered into a Cassandra native type. The + * returned type might be a subclass of the given expected type though. + * + * @param sourceType must not be {@literal null} + * @return + */ + public boolean hasCustomWriteTarget(Class sourceType) { + return hasCustomWriteTarget(sourceType, null); + } + + /** + * Returns whether we have a custom conversion registered to an object of the given source type + * into an object of the given Cassandra native target type. + * + * @param sourceType must not be {@literal null}. + * @param requestedTargetType + * @return + */ + public boolean hasCustomWriteTarget(Class sourceType, Class requestedTargetType) { + return getCustomWriteTarget(sourceType, requestedTargetType) != null; + } + + /** + * Returns whether we have a custom conversion registered to the given source into the given target + * type. + * + * @param sourceType must not be {@literal null} + * @param requestedTargetType must not be {@literal null} + * @return + */ + public boolean hasCustomReadTarget(Class sourceType, Class requestedTargetType) { + return getCustomReadTarget(sourceType, requestedTargetType) != null; + } + + /** + * Returns the actual target type for the given {@code sourceType} and {@code requestedTargetType}. Note that the + * returned {@link Class} could be an assignable type to the given {@code requestedTargetType}. + * + * @param sourceType must not be {@literal null}. + * @param requestedTargetType can be {@literal null}. + * @return + */ + private Class getCustomReadTarget(final Class sourceType, final Class requestedTargetType) { + + if (requestedTargetType == null) { + return null; + } + + return getOrCreateAndCache(new ConvertiblePair(sourceType, requestedTargetType), customReadTargetTypes, + new Producer() { + + @Override + public Class get() { + return getCustomTarget(sourceType, requestedTargetType, readingPairs); + } + }); + } + + /** + * Inspects the given {@link ConvertiblePair}s for ones that have a source compatible type as source. Additionally + * checks assignability of the target type if one is given. + * + * @param sourceType must not be {@literal null}. + * @param requestedTargetType can be {@literal null}. + * @param pairs must not be {@literal null}. + * @return + */ + private static Class getCustomTarget(Class sourceType, Class requestedTargetType, + Collection pairs) { + + Assert.notNull(sourceType); + Assert.notNull(pairs); + + if (requestedTargetType != null && pairs.contains(new ConvertiblePair(sourceType, requestedTargetType))) { + return requestedTargetType; + } + + for (ConvertiblePair typePair : pairs) { + if (typePair.getSourceType().isAssignableFrom(sourceType)) { + Class targetType = typePair.getTargetType(); + if (requestedTargetType == null || targetType.isAssignableFrom(requestedTargetType)) { + return targetType; + } + } + } + + return null; + } + + /** + * Will try to find a value for the given key in the given cache or produce one using the given {@link Producer} and + * store it in the cache. + * + * @param key the key to lookup a potentially existing value, must not be {@literal null}. + * @param cache the cache to find the value in, must not be {@literal null}. + * @param producer the {@link Producer} to create values to cache, must not be {@literal null}. + * @return + */ + private static Class getOrCreateAndCache(T key, Map>> cache, Producer producer) { + + CacheValue> cacheValue = cache.get(key); + + if (cacheValue != null) { + return cacheValue.getValue(); + } + + Class type = producer.get(); + cache.put(key, CacheValue.> ofNullable(type)); + + return type; + } + + private interface Producer { + Class get(); + } +} 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 cb35b8c22..044a0f735 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 @@ -1,12 +1,12 @@ /* * 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. * 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. @@ -55,11 +55,17 @@ import com.datastax.driver.core.querybuilder.Update; /** * {@link CassandraConverter} that uses a {@link MappingContext} to do sophisticated mapping of domain objects to * {@link Row}. - * + * * @author Alex Shvid * @author Matthew T. Adams * @author Oliver Gierke * @author Mark Paluch + * @see org.springframework.beans.factory.InitializingBean + * @see org.springframework.context.ApplicationContextAware + * @see org.springframework.beans.factory.BeanClassLoaderAware + * @see org.springframework.data.convert.EntityConverter + * @see org.springframework.data.convert.EntityReader + * @see org.springframework.data.convert.EntityWriter */ public class MappingCassandraConverter extends AbstractCassandraConverter implements CassandraConverter, ApplicationContextAware, BeanClassLoaderAware { @@ -80,7 +86,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter /** * Creates a new {@link MappingCassandraConverter} with the given {@link CassandraMappingContext}. - * + * * @param mappingContext must not be {@literal null}. */ public MappingCassandraConverter(CassandraMappingContext mappingContext) { @@ -106,6 +112,14 @@ public class MappingCassandraConverter extends AbstractCassandraConverter return (R) row; } + if (conversions.hasCustomReadTarget(Row.class, rawType) || conversionService.canConvert(Row.class, rawType)) { + return conversionService.convert(row, rawType); + } + + if (type.isCollectionLike() || type.isMap()) { + return conversionService.convert(row, clazz); + } + CassandraPersistentEntity persistentEntity = (CassandraPersistentEntity) mappingContext .getPersistentEntity(typeToUse); if (persistentEntity == null) { @@ -416,7 +430,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter /** * Creates a new {@link ConvertingPropertyAccessor} for the given source and entity. - * + * * @param source must not be {@literal null}. * @param entity must not be {@literal null}. * @return diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java index d0b7cc895..303d47741 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java @@ -68,6 +68,7 @@ import com.datastax.driver.core.querybuilder.Update; * @author Oliver Gierke * @author Mark Paluch * @see CqlTemplate + * @see CassandraOperations */ public class CassandraTemplate extends CqlTemplate implements CassandraOperations { @@ -79,44 +80,73 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation */ public CassandraTemplate() {} + /** + * Creates a new {@link} for the given {@link Session}. + * + * @param session must not be {@literal null}. + */ public CassandraTemplate(Session session) { - this(session, new MappingCassandraConverter()); + this(session, null); } /** * Constructor if only session and converter are known at time of Template Creation * - * @param session must not be {@literal null} + * @param session must not be {@literal null}. * @param converter must not be {@literal null}. */ public CassandraTemplate(Session session, CassandraConverter converter) { setSession(session); - setConverter(converter); + setConverter(converter != null ? converter : getDefaultCassandraConverter()); } + private static CassandraConverter getDefaultCassandraConverter() { + + MappingCassandraConverter mappingCassandraConverter = new MappingCassandraConverter(); + mappingCassandraConverter.afterPropertiesSet(); + return mappingCassandraConverter; + } + + /** + * Set the {@link CassandraConverter}. + * + * @param cassandraConverter must not be {@literal null}. + */ public void setConverter(CassandraConverter cassandraConverter) { - Assert.notNull(cassandraConverter); + Assert.notNull(cassandraConverter, "CassandraConverter must not be null!"); this.cassandraConverter = cassandraConverter; mappingContext = cassandraConverter.getMappingContext(); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.core.CassandraOperations#getConverter() + */ @Override public CassandraConverter getConverter() { return cassandraConverter; } + /** + * Returns the {@link CassandraMappingContext} + * + * @return the {@link CassandraMappingContext} + */ public CassandraMappingContext getCassandraMappingContext() { return mappingContext; } + /* (non-Javadoc) + * @see org.springframework.cassandra.support.CassandraAccessor#afterPropertiesSet() + */ @Override public void afterPropertiesSet() { + super.afterPropertiesSet(); - Assert.notNull(cassandraConverter); - Assert.notNull(mappingContext); + Assert.notNull(cassandraConverter, "CassandraConverter must not be null!"); + Assert.notNull(mappingContext, "CassandraMappingContext must not be null!"); } @Override @@ -218,8 +248,7 @@ public class CassandraTemplate extends CqlTemplate implements CassandraOperation public CqlIdentifier getTableName(Class entityClass) { if (entityClass == null) { - throw new InvalidDataAccessApiUsageException( - "No class parameter provided, entity table can't be determined!"); + throw new InvalidDataAccessApiUsageException("No class parameter provided, entity table can't be determined!"); } CassandraPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); 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 cd871f283..7868b4aae 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,12 +1,12 @@ /* * Copyright 2013-2014 the original author or authors - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -47,7 +47,7 @@ import com.datastax.driver.core.TableMetadata; /** * Default implementation of a {@link MappingContext} for Cassandra using {@link CassandraPersistentEntity} and * {@link CassandraPersistentProperty} as primary abstractions. - * + * * @author Alex Shvid * @author Matthew T. Adams */ @@ -70,7 +70,7 @@ public class BasicCassandraMappingContext extends * Creates a new {@link BasicCassandraMappingContext}. */ public BasicCassandraMappingContext() { - setSimpleTypeHolder(new CassandraSimpleTypeHolder()); + setSimpleTypeHolder(CassandraSimpleTypeHolder.HOLDER); } @Override diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java index 6e84e1ec5..9f129a84b 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/BasicCassandraPersistentEntityMetadataVerifier.java @@ -1,12 +1,12 @@ /* * Copyright 2013-2014 the original author or authors - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,7 +29,7 @@ import org.springframework.data.mapping.model.MappingException; /** * Default implementation for Cassandra Persistent Entity Verification. Ensures that annotated Persistent Entities will * map properly to a Cassandra Table. - * + * * @author Matthew T Adams * @author David Webb */ @@ -43,7 +43,7 @@ public class BasicCassandraPersistentEntityMetadataVerifier implements Cassandra public void verify(CassandraPersistentEntity entity) throws MappingException { VerifierMappingExceptions exceptions = new VerifierMappingExceptions(entity, - "Mapping Exceptions from BasicCassandraPersistentEntityMetadataVerifier"); + String.format("Mapping Exceptions from BasicCassandraPersistentEntityMetadataVerifier for %s", entity.getName())); final List idProperties = new ArrayList(); final List compositePrimaryKeys = new ArrayList(); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraSimpleTypeHolder.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraSimpleTypeHolder.java index 22bd2efd0..6c04f40b0 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraSimpleTypeHolder.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/mapping/CassandraSimpleTypeHolder.java @@ -1,12 +1,12 @@ /* * 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. * 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. @@ -27,10 +27,11 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.TypeInformation; import com.datastax.driver.core.DataType; +import com.datastax.driver.core.DataType.Name; /** * Simple constant holder for a {@link SimpleTypeHolder} enriched with Cassandra specific simple types. - * + * * @author Alex Shvid * @author Matthew T. Adams * @author Mark Paluch @@ -39,66 +40,120 @@ public class CassandraSimpleTypeHolder extends SimpleTypeHolder { public static final Set> CASSANDRA_SIMPLE_TYPES; - private static final Map, Class> primitiveTypesByWrapperType = new HashMap, Class>(8); - - private static final Map, DataType> dataTypesByJavaClass = new HashMap, DataType>(); - - private static final Map dataTypesByDataTypeName = new HashMap(); + private static final Map, DataType> classToDataType; + private static final Map nameToDataType; static { - primitiveTypesByWrapperType.put(Boolean.class, boolean.class); - primitiveTypesByWrapperType.put(Byte.class, byte.class); - primitiveTypesByWrapperType.put(Character.class, char.class); - primitiveTypesByWrapperType.put(Double.class, double.class); - primitiveTypesByWrapperType.put(Float.class, float.class); - primitiveTypesByWrapperType.put(Integer.class, int.class); - primitiveTypesByWrapperType.put(Long.class, long.class); - primitiveTypesByWrapperType.put(Short.class, short.class); + Map, Class> primitiveWrappers = new HashMap, Class>(8); + primitiveWrappers.put(Boolean.class, boolean.class); + primitiveWrappers.put(Byte.class, byte.class); + primitiveWrappers.put(Character.class, char.class); + primitiveWrappers.put(Double.class, double.class); + primitiveWrappers.put(Float.class, float.class); + primitiveWrappers.put(Integer.class, int.class); + primitiveWrappers.put(Long.class, long.class); + primitiveWrappers.put(Short.class, short.class); - Set> simpleTypes = new HashSet>(); + Set> simpleTypes = getCassandraPrimitiveTypes(); + simpleTypes.add(Number.class); + + classToDataType = Collections.unmodifiableMap(classToDataType(primitiveWrappers)); + nameToDataType = Collections.unmodifiableMap(nameToDataType()); + CASSANDRA_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); + } + + public static final SimpleTypeHolder HOLDER = new CassandraSimpleTypeHolder(); + + /** + * @return the map between {@link Name} and {@link DataType}. + */ + private static Map nameToDataType() { + + Map nameToDataType = new HashMap(16); + + for (DataType dataType : DataType.allPrimitiveTypes()) { + nameToDataType.put(dataType.getName(), dataType); + } + + return nameToDataType; + } + + /** + * @return the map between {@link Class} and {@link DataType}. + * @param primitiveWrappers + */ + private static Map, DataType> classToDataType(Map, Class> primitiveWrappers) { + + Map, DataType> classToDataType = new HashMap, DataType>(16); for (DataType dataType : DataType.allPrimitiveTypes()) { Class javaClass = dataType.asJavaClass(); - simpleTypes.add(javaClass); + classToDataType.put(javaClass, dataType); - dataTypesByJavaClass.put(javaClass, dataType); - - Class primitiveJavaClass = primitiveTypesByWrapperType.get(javaClass); + Class primitiveJavaClass = primitiveWrappers.get(javaClass); if (primitiveJavaClass != null) { - dataTypesByJavaClass.put(primitiveJavaClass, dataType); + classToDataType.put(primitiveJavaClass, dataType); } - - dataTypesByDataTypeName.put(dataType.getName(), dataType); } - dataTypesByJavaClass.put(String.class, DataType.text()); + // override String to text datatype as String is used multiple times + classToDataType.put(String.class, DataType.text()); - CASSANDRA_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); + return classToDataType; } + /** + * Returns a {@link Set} containing all Cassandra primitive types. + * + * @return + */ + private static Set> getCassandraPrimitiveTypes() { + + Set> simpleTypes = new HashSet>(); + for (DataType dataType : DataType.allPrimitiveTypes()) { + + Class javaClass = dataType.asJavaClass(); + simpleTypes.add(javaClass); + } + return simpleTypes; + } + + /** + * Returns the {@link DataType} for a {@link DataType.Name}. + * + * @param name + * @return + */ public static DataType getDataTypeFor(DataType.Name name) { - return dataTypesByDataTypeName.get(name); + return nameToDataType.get(name); } + /** + * Returns the default {@link DataType} for a {@link Class}. + * + * @param javaClass + * @return + */ public static DataType getDataTypeFor(Class javaClass) { if (javaClass.isEnum()) { return DataType.varchar(); } - return dataTypesByJavaClass.get(javaClass); + return classToDataType.get(javaClass); } public static DataType.Name[] getDataTypeNamesFrom(List> arguments) { + DataType.Name[] array = new DataType.Name[arguments.size()]; for (int i = 0; i != array.length; i++) { TypeInformation typeInfo = arguments.get(i); DataType dataType = getDataTypeFor(typeInfo.getType()); if (dataType == null) { - throw new InvalidDataAccessApiUsageException("not found appropriate primitive DataType for type = '" - + typeInfo.getType()); + throw new InvalidDataAccessApiUsageException( + String.format("Did not find appropriate primitive DataType for type '%s'", typeInfo.getType())); } array[i] = dataType.getName(); } 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 8308c8447..d9e447152 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 @@ -1,5 +1,5 @@ /* - * Copyright 2010-2014 the original author or authors. + * Copyright 2010-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. @@ -19,37 +19,24 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; -import java.util.List; -import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.cassandra.core.converter.ResultSetToBigDecimalConverter; -import org.springframework.cassandra.core.converter.ResultSetToBigIntegerConverter; -import org.springframework.cassandra.core.converter.ResultSetToBooleanConverter; -import org.springframework.cassandra.core.converter.ResultSetToByteBufferConverter; -import org.springframework.cassandra.core.converter.ResultSetToDateConverter; -import org.springframework.cassandra.core.converter.ResultSetToDoubleConverter; -import org.springframework.cassandra.core.converter.ResultSetToFloatConverter; -import org.springframework.cassandra.core.converter.ResultSetToInetAddressConverter; -import org.springframework.cassandra.core.converter.ResultSetToIntegerConverter; -import org.springframework.cassandra.core.converter.ResultSetToListConverter; -import org.springframework.cassandra.core.converter.ResultSetToLongConverter; -import org.springframework.cassandra.core.converter.ResultSetToStringConverter; -import org.springframework.cassandra.core.converter.ResultSetToUuidConverter; -import org.springframework.cassandra.core.converter.RowToMapConverter; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConfigurableConversionService; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.CollectionExecution; +import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultProcessingConverter; +import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultProcessingExecution; +import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultSetQuery; +import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.SingleEntityExecution; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -58,33 +45,22 @@ import com.datastax.driver.core.Row; /** * Base class for {@link RepositoryQuery} implementations for Cassandra. + * + * @author Mark Paluch */ public abstract class AbstractCassandraQuery implements RepositoryQuery { - protected static final Converter[] DEFAULT_CONVERTERS = new Converter[] { new ResultSetToListConverter(), - new ResultSetToStringConverter(), new RowToMapConverter(), new ResultSetToBigDecimalConverter(), - new ResultSetToBigIntegerConverter(), new ResultSetToBooleanConverter(), new ResultSetToByteBufferConverter(), - new ResultSetToDateConverter(), new ResultSetToDoubleConverter(), new ResultSetToFloatConverter(), - new ResultSetToInetAddressConverter(), new ResultSetToIntegerConverter(), new ResultSetToLongConverter(), - new ResultSetToUuidConverter() }; - protected static Logger log = LoggerFactory.getLogger(AbstractCassandraQuery.class); - private ConversionService conversionService; - - Converter>> resultSetToListConverter = new ResultSetToListConverter(); - private final CassandraQueryMethod method; private final CassandraOperations template; - protected RowToMapConverter rowToMapConverter = new RowToMapConverter(); - /** * Creates a new {@link AbstractCassandraQuery} from the given {@link CassandraQueryMethod} and * {@link CassandraOperations}. - * + * * @param method must not be {@literal null}. - * @param template must not be {@literal null}. + * @param operations must not be {@literal null}. */ public AbstractCassandraQuery(CassandraQueryMethod method, CassandraOperations operations) { @@ -93,19 +69,6 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { this.method = method; this.template = operations; - - this.conversionService = createDefaultConversionService(); - } - - protected ConfigurableConversionService createDefaultConversionService() { - - ConfigurableConversionService conversionService = new DefaultConversionService(); - - for (Converter converter : DEFAULT_CONVERTERS) { - conversionService.addConverter(converter); - } - - return conversionService; } @Override @@ -119,33 +82,46 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { CassandraParameterAccessor accessor = new CassandraParametersParameterAccessor(method, parameters); String query = createQuery(accessor); - ResultSet resultSet = template.query(query); + ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); - // return raw result set if requested - if (method.isResultSetQuery()) { - return resultSet; - } + CassandraQueryExecution cassandraQueryExecution = getExecution(query, accessor, + new ResultProcessingConverter(processor)); - Class declaredReturnType = method.getReturnType().getType(); - Class returnedUnwrappedObjectType = method.getReturnedObjectType(); - - if (method.isSingleEntityQuery()) { - return getSingleEntity(resultSet, returnedUnwrappedObjectType); - } - - Object retval = resultSet; - - if (method.isCollectionOfEntityQuery()) { - retval = getCollectionOfEntity(resultSet, declaredReturnType, returnedUnwrappedObjectType); - } - - // TODO: support Page & Slice queries - - // if we get this far, let the configured conversion service try to convert the result set - return conversionService.convert(retval, TypeDescriptor.forObject(retval), - TypeDescriptor.valueOf(declaredReturnType)); + return cassandraQueryExecution.execute(query, processor.getReturnedType().getReturnedType()); } + /** + * Returns the execution instance to use. + * + * @param query must not be {@literal null}. + * @param accessor must not be {@literal null}. + * @param resultProcessing must not be {@literal null}. @return + */ + private CassandraQueryExecution getExecution(String query, CassandraParameterAccessor accessor, + Converter resultProcessing) { + + return new ResultProcessingExecution(getExecutionToWrap(accessor), resultProcessing); + } + + private CassandraQueryExecution getExecutionToWrap(CassandraParameterAccessor accessor) { + + if (method.isResultSetQuery()) { + return new ResultSetQuery(template); + } else if (method.isCollectionQuery()) { + return new CollectionExecution(template); + } else { + return new SingleEntityExecution(template); + } + } + + /** + * @param resultSet + * @param declaredReturnType + * @param returnedUnwrappedObjectType + * @return + * @deprecated {@link org.springframework.data.cassandra.mapping.CassandraMappingContext} handles type conversion. + */ + @Deprecated public Object getCollectionOfEntity(ResultSet resultSet, Class declaredReturnType, Class returnedUnwrappedObjectType) { @@ -167,6 +143,13 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { return results; } + /** + * @param resultSet + * @param type + * @return + * @deprecated {@link org.springframework.data.cassandra.mapping.CassandraMappingContext} handles type conversion. + */ + @Deprecated public Object getSingleEntity(ResultSet resultSet, Class type) { if (resultSet.isExhausted()) { return null; @@ -194,18 +177,22 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { } public ConversionService getConversionService() { - return conversionService; + return template.getConverter().getConversionService(); } + /** + * @param conversionService + * @deprecated {@link org.springframework.data.cassandra.mapping.CassandraMappingContext} handles type conversion. + */ + @Deprecated public void setConversionService(ConversionService conversionService) { - - Assert.notNull(conversionService); - this.conversionService = conversionService; + throw new UnsupportedOperationException("setConversionService(ConversionService) is not supported anymore. " + + "Please use CassandraMappingContext instead"); } /** * Creates a string query using the given {@link ParameterAccessor} - * + * * @param accessor must not be {@literal null}. */ protected abstract String createQuery(CassandraParameterAccessor accessor); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java new file mode 100644 index 000000000..1880f9e50 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java @@ -0,0 +1,125 @@ +/* + * 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.core.convert.converter.Converter; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.util.ClassUtils; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +/** + * Query executions for Cassandra. + * + * @author Mark Paluch + * @since 1.5 + */ +interface CassandraQueryExecution { + + Object execute(String query, Class type); + + /** + * {@link CassandraQueryExecution} for collection returning queries. + * + * @author Mark Paluch + */ + @RequiredArgsConstructor + final class CollectionExecution implements CassandraQueryExecution { + + private final @NonNull CassandraOperations operations; + + @Override + public Object execute(String query, Class type) { + return operations.select(query, type); + } + } + + /** + * {@link CassandraQueryExecution} to return a single entity. + * + * @author Mark Paluch + */ + @RequiredArgsConstructor + final class SingleEntityExecution implements CassandraQueryExecution { + + private final @NonNull CassandraOperations operations; + + @Override + public Object execute(String query, Class type) { + return operations.selectOne(query, type); + } + } + + /** + * {@link CassandraQueryExecution} to return a {@link com.datastax.driver.core.ResultSet}. + * + * @author Mark Paluch + */ + @RequiredArgsConstructor + final class ResultSetQuery implements CassandraQueryExecution { + + private final @NonNull CassandraOperations operations; + + @Override + public Object execute(String query, Class type) { + return operations.query(query); + } + } + + /** + * An {@link CassandraQueryExecution} that wraps the results of the given delegate with the given result processing. + * + * @author Mark Paluch + */ + @RequiredArgsConstructor + final class ResultProcessingExecution implements CassandraQueryExecution { + + private final @NonNull CassandraQueryExecution delegate; + private final @NonNull Converter converter; + + @Override + public Object execute(String query, Class type) { + return converter.convert(delegate.execute(query, type)); + } + } + + /** + * A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}. + * + * @author Mark Paluch + */ + @RequiredArgsConstructor + final class ResultProcessingConverter implements Converter { + + private final @NonNull ResultProcessor processor; + + @Override + public Object convert(Object source) { + + ReturnedType returnedType = processor.getReturnedType(); + + if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) { + return source; + } + + return processor.processResult(source); + } + } +} 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 new file mode 100644 index 000000000..c03cfaa06 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/ConverterRegistrationUnitTests.java @@ -0,0 +1,79 @@ +/* + * 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link ConverterRegistration}. + * + * @author Mark Paluch + */ +public class ConverterRegistrationUnitTests { + + /** + * @see DATACASS-280 + */ + @Test + public void considersNotExplicitlyReadingDependingOnTypes() { + + ConverterRegistration context = new ConverterRegistration(Person.class, String.class, false, false); + assertThat(context.isWriting(), is(true)); + assertThat(context.isReading(), is(false)); + + context = new ConverterRegistration(String.class, Person.class, false, false); + assertThat(context.isWriting(), is(false)); + assertThat(context.isReading(), is(true)); + + context = new ConverterRegistration(String.class, Class.class, false, false); + assertThat(context.isWriting(), is(true)); + assertThat(context.isReading(), is(true)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void forcesReadWriteOnlyIfAnnotated() { + + ConverterRegistration context = new ConverterRegistration(String.class, Class.class, false, true); + assertThat(context.isWriting(), is(true)); + assertThat(context.isReading(), is(false)); + + context = new ConverterRegistration(String.class, Class.class, true, false); + assertThat(context.isWriting(), is(false)); + assertThat(context.isReading(), is(true)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void considersConverterForReadAndWriteIfBothAnnotated() { + + ConverterRegistration context = new ConverterRegistration(String.class, Class.class, true, true); + 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 new file mode 100644 index 000000000..aafe1fceb --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/convert/CustomConversionsUnitTests.java @@ -0,0 +1,306 @@ +/* + * 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 static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.net.InetAddress; +import java.text.DateFormat; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; +import java.util.UUID; + +import org.joda.time.DateTime; +import org.junit.Test; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.core.convert.converter.Converter; +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; + +/** + * Unit tests for {@link CustomConversions}. + * + * @soundtrack Atc - Why Oh Why (Extended Version) + * @author Mark Paluch + */ +public class CustomConversionsUnitTests { + + /** + * @see DATACASS-280 + */ + @Test + public void findsBasicReadAndWriteConversions() { + + CustomConversions conversions = new CustomConversions( + Arrays.asList(FormatToStringConverter.INSTANCE, StringToFormatConverter.INSTANCE)); + + assertThat(conversions.getCustomWriteTarget(Format.class, null), is(typeCompatibleWith(String.class))); + assertThat(conversions.getCustomWriteTarget(String.class, null), is(nullValue())); + + assertThat(conversions.hasCustomReadTarget(String.class, Format.class), is(true)); + assertThat(conversions.hasCustomReadTarget(String.class, Locale.class), is(false)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void considersSubtypesCorrectly() { + + CustomConversions conversions = new CustomConversions( + Arrays.asList(NumberToStringConverter.INSTANCE, StringToNumberConverter.INSTANCE)); + + assertThat(conversions.getCustomWriteTarget(Long.class, null), is(typeCompatibleWith(String.class))); + assertThat(conversions.hasCustomReadTarget(String.class, Long.class), is(true)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void considersTypesWeRegisteredConvertersForAsSimple() { + + CustomConversions conversions = new CustomConversions(Arrays.asList(FormatToStringConverter.INSTANCE)); + assertThat(conversions.isSimpleType(UUID.class), is(true)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void populatesConversionServiceCorrectly() { + + GenericConversionService conversionService = new DefaultConversionService(); + + CustomConversions conversions = new CustomConversions(Arrays.asList(StringToFormatConverter.INSTANCE)); + conversions.registerConvertersIn(conversionService); + + assertThat(conversionService.canConvert(String.class, Format.class), is(true)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void doesNotConsiderTypeSimpleIfOnlyReadConverterIsRegistered() { + + CustomConversions conversions = new CustomConversions(Arrays.asList(StringToFormatConverter.INSTANCE)); + assertThat(conversions.isSimpleType(Format.class), is(false)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void discoversConvertersForSubtypesOfCassandraTypes() { + + CustomConversions conversions = new CustomConversions(Arrays.asList(StringToIntegerConverter.INSTANCE)); + assertThat(conversions.hasCustomReadTarget(String.class, Integer.class), is(true)); + assertThat(conversions.hasCustomWriteTarget(String.class, Integer.class), is(true)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void considersUUIDASimpleType() { + + CustomConversions conversions = new CustomConversions(); + assertThat(conversions.isSimpleType(UUID.class), is(true)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void considersInetAddressASimpleType() { + + CustomConversions conversions = new CustomConversions(); + assertThat(conversions.isSimpleType(InetAddress.class), is(true)); + } + + /** + * @see DATACASS-280 + */ + @Test + @SuppressWarnings("rawtypes") + public void favorsCustomConverterForIndeterminedTargetType() { + + CustomConversions conversions = new CustomConversions(Arrays.asList(DateTimeToStringConverter.INSTANCE)); + assertThat(conversions.getCustomWriteTarget(DateTime.class, null), is(equalTo((Class) String.class))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void customConverterOverridesDefault() { + + CustomConversions conversions = new CustomConversions(Arrays.asList(CustomDateTimeConverter.INSTANCE)); + GenericConversionService conversionService = new DefaultConversionService(); + conversions.registerConvertersIn(conversionService); + + assertThat(conversionService.convert(new DateTime(), Date.class), is(new Date(0))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldSelectPropertCustomWriteTargetForCglibProxiedType() { + + CustomConversions conversions = new CustomConversions(Arrays.asList(FormatToStringConverter.INSTANCE)); + assertThat(conversions.getCustomWriteTarget(createProxyTypeFor(Format.class)), + is(typeCompatibleWith(String.class))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldSelectPropertyCustomReadTargetForCglibProxiedType() { + + CustomConversions conversions = new CustomConversions(Arrays.asList(CustomObjectToStringConverter.INSTANCE)); + assertThat(conversions.hasCustomReadTarget(createProxyTypeFor(Object.class), String.class), is(true)); + } + + /** + * @see DATACASS-280 + */ + @Test + public void registersConverterFactoryCorrectly() { + + CustomConversions customConversions = new CustomConversions( + Collections.singletonList(new FormatConverterFactory())); + + assertThat(customConversions.getCustomWriteTarget(String.class, SimpleDateFormat.class), notNullValue()); + } + + private static Class createProxyTypeFor(Class type) { + + ProxyFactory factory = new ProxyFactory(); + factory.setProxyTargetClass(true); + factory.setTargetClass(type); + + return factory.getProxy().getClass(); + } + + enum FormatToStringConverter implements Converter { + INSTANCE; + + public String convert(Format source) { + return source.toString(); + } + } + + enum StringToFormatConverter implements Converter { + INSTANCE; + public Format convert(String source) { + return DateFormat.getInstance(); + } + } + + enum NumberToStringConverter implements Converter { + INSTANCE; + public String convert(Number source) { + return source.toString(); + } + } + + enum StringToNumberConverter implements Converter { + INSTANCE; + public Number convert(String source) { + return 0L; + } + } + + enum StringToIntegerConverter implements Converter { + INSTANCE; + public Integer convert(String source) { + return 0; + } + } + + enum DateTimeToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(DateTime source) { + return ""; + } + } + + enum CustomDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public Date convert(DateTime source) { + return new Date(0); + } + } + + enum CustomObjectToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(Object source) { + return source != null ? source.toString() : null; + } + + } + + @WritingConverter + static class FormatConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + return new StringToFormat(targetType); + } + + private static final class StringToFormat implements Converter { + + private final Class targetType; + + public StringToFormat(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(String source) { + + if (source.length() == 0) { + return null; + } + + try { + return targetType.newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + } + } +} 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 2d9ae4464..d4989fc8a 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 @@ -18,18 +18,33 @@ package org.springframework.data.cassandra.convert; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assume.*; +import static org.mockito.Mockito.*; import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.UUID; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import org.springframework.cassandra.core.PrimaryKeyType; import org.springframework.core.SpringVersion; import org.springframework.core.convert.ConverterNotFoundException; +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.cassandra.mapping.PrimaryKey; import org.springframework.data.cassandra.mapping.PrimaryKeyClass; @@ -37,7 +52,9 @@ import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; 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.Name; +import com.datastax.driver.core.Row; import com.datastax.driver.core.querybuilder.Assignment; import com.datastax.driver.core.querybuilder.BuiltStatement; import com.datastax.driver.core.querybuilder.Clause; @@ -53,11 +70,24 @@ import com.datastax.driver.core.querybuilder.Update.Assignments; * @author Mark Paluch * @soundtrack Outlandich - Dont Leave Me Feat Cyt (Sun Kidz Electrocore Mix) */ +@RunWith(MockitoJUnitRunner.class) public class MappingCassandraConverterUnitTests { @Rule public final ExpectedException expectedException = ExpectedException.none(); + @Mock private Row rowMock; + @Mock private ColumnDefinitions columnDefinitionsMock; - private MappingCassandraConverter mappingCassandraConverter = new MappingCassandraConverter(); + private CassandraMappingContext mappingContext; + private MappingCassandraConverter mappingCassandraConverter; + + @Before + public void setUp() throws Exception { + + mappingContext = new BasicCassandraMappingContext(); + mappingCassandraConverter = new MappingCassandraConverter(mappingContext); + + mappingCassandraConverter.afterPropertiesSet(); + } /** * @see DATACASS-260 @@ -233,6 +263,152 @@ public class MappingCassandraConverterUnitTests { assertThat(getWhereValues(where), contains((Object) "MINT")); } + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadStringCorrectly() { + + when(rowMock.getString(0)).thenReturn("foo"); + + String result = mappingCassandraConverter.readRow(String.class, rowMock); + + assertThat(result, is(equalTo("foo"))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadIntegerCorrectly() { + + when(rowMock.getObject(0)).thenReturn(2); + + Integer result = mappingCassandraConverter.readRow(Integer.class, rowMock); + + assertThat(result, is(equalTo(2))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadLongCorrectly() { + + when(rowMock.getObject(0)).thenReturn(2); + + Long result = mappingCassandraConverter.readRow(Long.class, rowMock); + + assertThat(result, is(equalTo(2L))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadDoubleCorrectly() { + + when(rowMock.getObject(0)).thenReturn(2D); + + Double result = mappingCassandraConverter.readRow(Double.class, rowMock); + + assertThat(result, is(equalTo(2D))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadFloatCorrectly() { + + when(rowMock.getObject(0)).thenReturn(2F); + + Float result = mappingCassandraConverter.readRow(Float.class, rowMock); + + assertThat(result, is(equalTo(2F))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadBigIntegerCorrectly() { + + when(rowMock.getObject(0)).thenReturn(BigInteger.valueOf(2)); + + BigInteger result = mappingCassandraConverter.readRow(BigInteger.class, rowMock); + + assertThat(result, is(equalTo(BigInteger.valueOf(2)))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadBigDecimalCorrectly() { + + when(rowMock.getObject(0)).thenReturn(BigDecimal.valueOf(2)); + + BigDecimal result = mappingCassandraConverter.readRow(BigDecimal.class, rowMock); + + assertThat(result, is(equalTo(BigDecimal.valueOf(2)))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadUUIDCorrectly() { + + UUID uuid = UUID.randomUUID(); + when(rowMock.getUUID(0)).thenReturn(uuid); + + UUID result = mappingCassandraConverter.readRow(UUID.class, rowMock); + + assertThat(result, is(equalTo(uuid))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadInetAddressCorrectly() throws UnknownHostException { + + InetAddress localHost = InetAddress.getLocalHost(); + when(rowMock.getInet(0)).thenReturn(localHost); + + InetAddress result = mappingCassandraConverter.readRow(InetAddress.class, rowMock); + + assertThat(result, is(equalTo(localHost))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadDateCorrectly() throws UnknownHostException { + + Date date = new Date(1); + when(rowMock.getDate(0)).thenReturn(date); + + Date result = mappingCassandraConverter.readRow(Date.class, rowMock); + + assertThat(result, is(equalTo(date))); + } + + /** + * @see DATACASS-280 + */ + @Test + public void shouldReadBooleanCorrectly() throws UnknownHostException { + + when(rowMock.getBool(0)).thenReturn(true); + + Boolean result = mappingCassandraConverter.readRow(Boolean.class, rowMock); + + assertThat(result, is(equalTo(true))); + } + @SuppressWarnings("unchecked") private List getValues(Insert statement) { return (List) ReflectionTestUtils.getField(statement, "values"); @@ -290,7 +466,6 @@ public class MappingCassandraConverterUnitTests { public void setAsOrdinal(Condition asOrdinal) { this.asOrdinal = asOrdinal; } - } @Table @@ -315,7 +490,6 @@ public class MappingCassandraConverterUnitTests { public void setCondition(Condition condition) { this.condition = condition; } - } @PrimaryKeyClass @@ -323,7 +497,8 @@ public class MappingCassandraConverterUnitTests { @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED) private Condition condition; - public EnumCompositePrimaryKey() {} + public EnumCompositePrimaryKey() { + } public EnumCompositePrimaryKey(Condition condition) { this.condition = condition; @@ -336,7 +511,6 @@ public class MappingCassandraConverterUnitTests { public void setCondition(Condition condition) { this.condition = condition; } - } @Table @@ -351,7 +525,6 @@ public class MappingCassandraConverterUnitTests { public void setCondition(Condition condition) { this.condition = condition; } - } @Table @@ -359,7 +532,8 @@ public class MappingCassandraConverterUnitTests { @PrimaryKey private EnumCompositePrimaryKey key; - public CompositeKeyThing() {} + public CompositeKeyThing() { + } public CompositeKeyThing(EnumCompositePrimaryKey key) { this.key = key; @@ -372,11 +546,9 @@ public class MappingCassandraConverterUnitTests { public void setKey(EnumCompositePrimaryKey key) { this.key = key; } - } public static enum Condition { MINT, USED; } - } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java index 4e38000c5..271c41fa4 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/core/CassandraOperationsIntegrationTests.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -688,4 +688,16 @@ public class CassandraOperationsIntegrationTests extends AbstractSpringDataEmbed Assert.assertEquals(count, template.count(Book.class)); } + + @Test + public void insertAndSelect() { + + int count = 20; + List books = getBookList(count); + + template.insert(books); + + Assert.assertEquals(count, template.count(Book.class)); + } + } 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 new file mode 100644 index 000000000..61e904e78 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/types/CassandraTypeMappingIntegrationTest.java @@ -0,0 +1,509 @@ +/* + * 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.types; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.mapping.CassandraType; +import org.springframework.data.cassandra.mapping.PrimaryKey; +import org.springframework.data.cassandra.mapping.Table; +import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; +import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.datastax.driver.core.DataType.Name; + +/** + * Integration tests for type mapping using {@link CassandraOperations}. + * + * @author Mark Paluch + * @soundtrack DJ THT meets Scarlet - Live 2 Dance (Extended Mix) (Zgin Remix) + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class CassandraTypeMappingIntegrationTest extends AbstractSpringDataEmbeddedCassandraIntegrationTest { + + @Configuration + public static class Config extends IntegrationTestConfig { + + @Override + public String[] getEntityBasePackages() { + return new String[] { AllPossibleTypes.class.getPackage().getName() }; + } + } + + @Autowired CassandraOperations cassandraOperations; + + @Before + public void setUp() { + cassandraOperations.deleteAll(AllPossibleTypes.class); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteInetAddress() throws Exception { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setInet(InetAddress.getByName("127.0.0.1")); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getInet(), is(equalTo(entity.getInet()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteUUID() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setUuid(UUID.randomUUID()); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getUuid(), is(equalTo(entity.getUuid()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteBoxedLongShort() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBoxedShort(Short.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBoxedShort(), is(equalTo(entity.getBoxedShort()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWritePrimitiveShort() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setPrimitiveShort(Short.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getPrimitiveShort(), is(equalTo(entity.getPrimitiveShort()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteBoxedLong() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBoxedLong(Long.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBoxedLong(), is(equalTo(entity.getBoxedLong()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWritePrimitiveLong() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setPrimitiveLong(Long.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getPrimitiveLong(), is(equalTo(entity.getPrimitiveLong()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteBoxedInteger() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBoxedInteger(Integer.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBoxedInteger(), is(equalTo(entity.getBoxedInteger()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWritePrimitiveInteger() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setPrimitiveInteger(Integer.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getPrimitiveInteger(), is(equalTo(entity.getPrimitiveInteger()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteBoxedFloat() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBoxedFloat(Float.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBoxedFloat(), is(equalTo(entity.getBoxedFloat()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWritePrimitiveFloat() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setPrimitiveFloat(Float.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getPrimitiveFloat(), is(equalTo(entity.getPrimitiveFloat()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteBoxedDouble() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBoxedDouble(Double.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBoxedDouble(), is(equalTo(entity.getBoxedDouble()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWritePrimitiveDouble() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setPrimitiveDouble(Double.MAX_VALUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getPrimitiveDouble(), is(equalTo(entity.getPrimitiveDouble()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteBoxedBoolean() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBoxedBoolean(Boolean.TRUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBoxedBoolean(), is(equalTo(entity.getBoxedBoolean()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWritePrimitiveBoolean() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setPrimitiveBoolean(Boolean.TRUE); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.isPrimitiveBoolean(), is(equalTo(entity.isPrimitiveBoolean()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteDate() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setTimestamp(new Date(1)); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getTimestamp(), is(equalTo(entity.getTimestamp()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteBigInteger() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBigInteger(new BigInteger("123456")); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBigInteger(), is(equalTo(entity.getBigInteger()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteBigDecimal() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBigDecimal(new BigDecimal("123456.7890123")); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getBigDecimal(), is(equalTo(entity.getBigDecimal()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteBlob() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setBlob(ByteBuffer.wrap("Hello".getBytes())); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + ByteBuffer blob = loaded.getBlob(); + byte[] bytes = new byte[blob.remaining()]; + blob.get(bytes); + assertThat(new String(bytes), is(equalTo("Hello"))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteSetOfString() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setSetOfString(Collections.singleton("hello")); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getSetOfString(), is(equalTo(entity.getSetOfString()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteEmptySetOfString() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setSetOfString(new HashSet()); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getSetOfString(), is(nullValue())); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteListOfString() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setListOfString(Collections.singletonList("hello")); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getListOfString(), is(equalTo(entity.getListOfString()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteEmptyListOfString() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setListOfString(new ArrayList()); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getListOfString(), is(nullValue())); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteMapOfString() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setMapOfString(Collections.singletonMap("hello", "world")); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getMapOfString(), is(equalTo(entity.getMapOfString()))); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteEmptyMapOfString() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setMapOfString(new HashMap()); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getMapOfString(), is(nullValue())); + } + + /** + * see DATACASS-280. + */ + @Test + public void shouldReadAndWriteEnum() { + + AllPossibleTypes entity = new AllPossibleTypes("1"); + entity.setAnEnum(Condition.MINT); + + cassandraOperations.insert(entity); + AllPossibleTypes loaded = cassandraOperations.selectOneById(AllPossibleTypes.class, entity.getId()); + + assertThat(loaded.getAnEnum(), is(equalTo(entity.getAnEnum()))); + } + + @Table + @Data + @NoArgsConstructor + @RequiredArgsConstructor + public static class AllPossibleTypes { + + @PrimaryKey @NonNull private String id; + + private InetAddress inet; + + @CassandraType(type = Name.UUID) private UUID uuid; + + @CassandraType(type = Name.INT) private Number justNumber; + + @CassandraType(type = Name.INT) private Short boxedShort; + @CassandraType(type = Name.INT) private short primitiveShort; + + @CassandraType(type = Name.BIGINT) private Long boxedLong; + @CassandraType(type = Name.BIGINT) private long primitiveLong; + + private Integer boxedInteger; + private int primitiveInteger; + + private Float boxedFloat; + private float primitiveFloat; + + private Double boxedDouble; + private double primitiveDouble; + + private Boolean boxedBoolean; + private boolean primitiveBoolean; + + private Date timestamp; + private BigDecimal bigDecimal; + private BigInteger bigInteger; + private ByteBuffer blob; + + private Set setOfString; + private List listOfString; + private Map mapOfString; + + private Condition anEnum; + + } + + public static enum Condition { + MINT, USED; + } +} 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 85ef492f3..59b355efc 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 @@ -17,6 +17,8 @@ package org.springframework.data.cassandra.test.integration.repository.querymeth import java.util.Date; +import lombok.Data; + import org.springframework.cassandra.core.PrimaryKeyType; import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; import org.springframework.data.cassandra.mapping.Table; @@ -25,6 +27,7 @@ import org.springframework.data.cassandra.mapping.Table; * Sample domain class. */ @Table +@Data public class Person { @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 0) private String lastname; @@ -32,68 +35,7 @@ public class Person { @PrimaryKeyColumn(type = PrimaryKeyType.CLUSTERED, ordinal = 1) private String firstname; private String nickname; - private Date birthDate; - private int numberOfChildren; - private boolean cool; - - // TODO: private UUID uuid = UUID.randomUUID(); - - public String getFirstname() { - return firstname; - } - - public void setFirstname(String firstname) { - this.firstname = firstname; - } - - public String getLastname() { - return lastname; - } - - public void setLastname(String lastname) { - this.lastname = lastname; - } - - public String getNickname() { - return nickname; - } - - public void setNickname(String nickname) { - this.nickname = nickname; - } - - public Date getBirthDate() { - return new Date(birthDate.getTime()); - } - - public void setBirthDate(Date birthDate) { - this.birthDate = birthDate == null ? null : new Date(birthDate.getTime()); - } - - public int getNumberOfChildren() { - return numberOfChildren; - } - - public void setNumberOfChildren(int numberOfChildren) { - this.numberOfChildren = numberOfChildren; - } - - public boolean isCool() { - return cool; - } - - public void setCool(boolean cool) { - this.cool = cool; - } - - // public UUID getUuid() { - // return uuid; - // } - // - // public void setUuid(UUID uuid) { - // this.uuid = uuid; - // } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/QueryIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/QueryIntegrationTests.java index 2e062d4db..cb31400fc 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 @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Optional; import org.junit.Before; import org.junit.Test; @@ -284,4 +285,28 @@ public abstract class QueryIntegrationTests extends AbstractSpringDataEmbeddedCa assertEquals(saved.getFirstname(), person.getFirstname()); } } + + @Test + public void findOptionalShouldReturnTargetType() { + + Person saved = new Person(); + saved.setFirstname(uuid()); + saved.setLastname(uuid()); + saved.setNumberOfChildren(1); + + saved = personRepository.save(saved); + + Optional optional = personRepository.findOptionalWithLastnameAndFirstname(saved.getLastname(), saved.getFirstname()); + + assertTrue(optional.isPresent()); + assertTrue(optional.get() instanceof Person); + } + + @Test + public void findOptionalShouldAbsentOptional() { + + Optional optional = personRepository.findOptionalWithLastnameAndFirstname("not", "existent"); + + assertFalse(optional.isPresent()); + } } 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 5d486130a..a09b8d70f 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 @@ -18,6 +18,7 @@ package org.springframework.data.cassandra.test.integration.repository.querymeth import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Optional; import org.springframework.data.cassandra.repository.Query; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; @@ -68,4 +69,8 @@ public interface PersonRepositoryWithQueryAnnotations extends PersonRepository { @Override @Query("select numberofchildren from person where lastname = ?0 and firstname = ?1") int findSingleNumberOfChildren(String last, String first); + + @Override + @Query("select * from person where lastname = ?0 and firstname = ?1") + Optional findOptionalWithLastnameAndFirstname(String last, String first); } 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 f4da84590..26cb0eb36 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 @@ -18,6 +18,7 @@ package org.springframework.data.cassandra.test.integration.repository.querymeth import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Optional; import org.springframework.data.cassandra.repository.CassandraRepository; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; @@ -48,4 +49,7 @@ public interface PersonRepository extends CassandraRepository { boolean findSingleCool(String last, String first); int findSingleNumberOfChildren(String last, String first); + + Optional findOptionalWithLastnameAndFirstname(String last, String first); + } 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 cc6191e89..3c074f895 100644 --- a/spring-data-cassandra/src/test/resources/META-INF/PersonRepositoryWithNamedQueries.properties +++ b/spring-data-cassandra/src/test/resources/META-INF/PersonRepositoryWithNamedQueries.properties @@ -7,3 +7,5 @@ Person.findSingleNickname=select nickname from person where lastname = ?0 and fi Person.findSingleBirthdate=select birthdate from person where lastname = ?0 and firstname = ?1 Person.findSingleCool=select cool from person where lastname = ?0 and firstname = ?1 Person.findSingleNumberOfChildren=select numberofchildren from person where lastname = ?0 and firstname = ?1 +Person.findOptionalWithLastnameAndFirstname=select * from person where lastname = ?0 and firstname = ?1 + diff --git a/spring-data-cassandra/template.mf b/spring-data-cassandra/template.mf index 8b6db04ba..cc8eb300d 100644 --- a/spring-data-cassandra/template.mf +++ b/spring-data-cassandra/template.mf @@ -2,6 +2,8 @@ Bundle-SymbolicName: org.springframework.data.cassandra Bundle-Name: Spring Data Cassandra Bundle-Vendor: Spring Data Cassandra Community Bundle-ManifestVersion: 2 +Excluded-Imports: + lombok.* Import-Package: sun.reflect;version="0";resolution:=optional Import-Template: @@ -29,4 +31,4 @@ Import-Template: 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 + com.google.common.*;resolution:="optional";version="[11.0.0, 20.0.0)"