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)"