/*
* Copyright (C) 2012-2015 DataStax Inc.
*
* 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 com.datastax.driver.mapping;
import com.datastax.driver.core.*;
import com.datastax.driver.core.exceptions.InvalidTypeException;
import com.datastax.driver.core.utils.CassandraVersion;
import com.datastax.driver.mapping.annotations.*;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeParameter;
import com.google.common.reflect.TypeToken;
import org.testng.annotations.Test;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
@CassandraVersion("2.1.0")
@SuppressWarnings("unused")
public class MapperCustomCodecTest extends CCMTestsSupport {
@Override
public void onTestContextInitialized() {
execute(
// Columns mapped to custom types
"CREATE TABLE data1 (i int PRIMARY KEY, l bigint)",
"INSERT INTO data1 (i, l) VALUES (1, 11)",
// UDT fields mapped to custom types
"CREATE TYPE holder(i int, l bigint)",
"CREATE TABLE data2(i int primary key, data frozen<holder>)",
"INSERT INTO data2 (i, data) values (1, {i: 1, l: 11})",
// nested UDT
// both UDT fields and non-UDT elements in the collection are mapped to custom types
"CREATE TABLE data3(i int primary key, data map<int, frozen<holder>>)",
"INSERT INTO data3 (i, data) values (1, {1: {i: 1, l: 11}})"
);
}
@Override
public Cluster.Builder createClusterBuilder() {
return Cluster.builder().withCodecRegistry(new CodecRegistry()
.register(new CustomInt.Codec()));
}
@Test(groups = "short")
public void should_use_custom_codecs_with_basic_operations() {
Mapper<Data1> mapper = new MappingManager(session()).mapper(Data1.class);
// Get
Data1 data11 = mapper.get(new CustomInt(1));
assertThat(data11.getI()).isEqualTo(new CustomInt(1));
assertThat(data11.getL()).isEqualTo(new CustomLong(11));
// Save
Data1 data12 = new Data1();
data12.setI(new CustomInt(2));
data12.setL(new CustomLong(12));
mapper.save(data12);
Row row = session().execute("select * from data1 where i = 2").one();
assertThat(row.getInt(0)).isEqualTo(2);
assertThat(row.getLong(1)).isEqualTo(12);
// Delete
mapper.delete(new CustomInt(2));
row = session().execute("select * from data1 where i = 2").one();
assertThat(row).isNull();
}
@Test(groups = "short")
public void should_use_custom_codecs_with_accessors() {
Data1Accessor accessor = new MappingManager(session()).createAccessor(Data1Accessor.class);
Data1 data1 = accessor.get(new CustomInt(1));
assertThat(data1.getI()).isEqualTo(new CustomInt(1));
assertThat(data1.getL()).isEqualTo(new CustomLong(11));
accessor.setL(1, new CustomLong(12));
Row row = session().execute("select l from data1 where i = 1").one();
assertThat(row.getLong(0)).isEqualTo(12);
accessor.setL(1, new CustomLong(11));
}
@Test(groups = "short")
public void should_use_custom_codecs_in_UDTs() {
Mapper<Data2> mapper = new MappingManager(session()).mapper(Data2.class);
// Get
Data2 data21 = mapper.get(1);
assertThat(data21.getData().getI()).isEqualTo(new CustomInt(1));
assertThat(data21.getData().getL()).isEqualTo(new CustomLong(11));
// Save
Data2 data22 = new Data2();
data22.setI(2);
data22.setData(new Holder(2, 22));
mapper.save(data22);
Row row = session().execute("select * from data2 where i = 2").one();
assertThat(row.getUDTValue(1).getInt("i")).isEqualTo(2);
assertThat(row.getUDTValue(1).getLong("l")).isEqualTo(22);
// cleanup
mapper.delete(2);
}
@Test(groups = "short")
public void should_use_custom_codecs_in_nested_structures() {
Mapper<Data3> mapper = new MappingManager(session()).mapper(Data3.class);
// Get
Data3 data31 = mapper.get(1);
assertThat(data31.getData().containsKey(new CustomInt(1)));
assertThat(data31.getData().get(new CustomInt(1)).getI()).isEqualTo(new CustomInt(1));
// Save
Data3 data32 = new Data3();
data32.setI(2);
data32.setData(ImmutableMap.of(new CustomInt(2), new Holder(2, 22)));
mapper.save(data32);
Row row = session().execute("select * from data3 where i = 2").one();
Map<Integer, UDTValue> data = row.getMap(1, Integer.class, UDTValue.class);
assertThat(data.containsKey(2));
assertThat(data.get(2).getInt("i")).isEqualTo(2);
assertThat(data.get(2).getLong("l")).isEqualTo(22);
// cleanup
mapper.delete(2);
}
@Test(groups = "short", expectedExceptions = IllegalArgumentException.class)
public void should_fail_when_invalid_codec_with_no_default_ctor_provided() {
new MappingManager(session()).mapper(Data1InvalidCodecNoDefaultConstructor.class);
}
@Test(groups = "short", expectedExceptions = InvalidTypeException.class)
public void should_fail_when_invalid_codec_type_mapping() {
Mapper<Data1InvalidCodecTypeMapping> mapper = new MappingManager(session()).mapper(Data1InvalidCodecTypeMapping.class);
// Get should return an InvalidTypeException.
try {
mapper.get(1);
fail("Should not have been able to retrieve.");
} catch (InvalidTypeException e) {
// expected.
}
// Set should also return an InvalidTypeException.
Data1InvalidCodecTypeMapping data12 = new Data1InvalidCodecTypeMapping();
data12.setI(2);
data12.setL(Optional.of(1L));
mapper.save(data12);
}
@Test(groups = "short", expectedExceptions = IllegalArgumentException.class)
public void should_fail_when_invalid_codec_with_no_default_ctor_provided_in_accessor() {
new MappingManager(session()).createAccessor(Data1AccessorNoDefaultConstructor.class);
}
@Test(groups = "short", expectedExceptions = InvalidTypeException.class)
public void should_fail_when_invalid_codec_type_mapping_in_accessor() {
Data1AccessorInvalidCodecTypeMapping accessor = new MappingManager(session()).createAccessor(Data1AccessorInvalidCodecTypeMapping.class);
// Get should return an InvalidTypeException.
try {
accessor.get(1);
fail("Should not have been able to retrieve.");
} catch (InvalidTypeException e) {
// expected.
}
accessor.setL(1, Optional.of(2L));
}
@Test(groups = "short")
public void should_be_able_to_use_parameterized_type() {
Mapper<Data1ParameterizedType> mapper = new MappingManager(session()).mapper(Data1ParameterizedType.class);
Data1ParameterizedType data1 = mapper.get(1);
assertThat(data1.getL()).isEqualTo(Optional.of(11L));
// Set with an absent ('null') value.
Data1ParameterizedType empty1 = new Data1ParameterizedType();
empty1.setI(1000);
empty1.setL(Optional.<Long>absent());
mapper.save(empty1);
// Value should come back absent.
Data1ParameterizedType empty1r = mapper.get(1000);
assertThat(empty1r.getL()).isEqualTo(Optional.<Long>absent());
// Value should come back absent with codec, otherwise null with getObject, 0 with getLong.
Row row = session().execute("select l from data1 where i=1000").one();
assertThat(row.getObject(0)).isEqualTo(null); // should map to long codec and return null.
assertThat(row.getLong(0)).isEqualTo(0L); // default boxed primitive value.
assertThat(row.get(0, new OptionalOfLong())).isEqualTo(Optional.<Long>absent());
// Set with a present value.
Data1ParameterizedType present1 = new Data1ParameterizedType();
present1.setI(1001);
present1.setL(Optional.of(20L));
mapper.save(present1);
// Value should come back present with codec, otherwise 20L.
row = session().execute("select l from data1 where i=1001").one();
assertThat(row.getObject(0)).isEqualTo(20L); // should map to long codec.
assertThat(row.getLong(0)).isEqualTo(20L);
assertThat(row.get(0, new OptionalOfLong())).isEqualTo(Optional.of(20L));
}
@Table(name = "data1")
public static class Data1 {
@PartitionKey
private CustomInt i;
@Column(codec = CustomLong.Codec.class)
private CustomLong l;
public CustomInt getI() {
return i;
}
public void setI(CustomInt i) {
this.i = i;
}
public CustomLong getL() {
return l;
}
public void setL(CustomLong l) {
this.l = l;
}
}
@Table(name = "data1")
public static class Data1InvalidCodecNoDefaultConstructor {
@PartitionKey
private int i;
@Column(codec = NoDefaultConstructorCodec.class)
private long l;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
public long getL() {
return l;
}
public void setL(long l) {
this.l = l;
}
}
@Table(name = "data1")
public static class Data1InvalidCodecTypeMapping {
@PartitionKey
private int i;
@Column(codec = OptionalOfString.class)
private Optional<Long> l;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
public Optional<Long> getL() {
return l;
}
public void setL(Optional<Long> l) {
this.l = l;
}
}
@Table(name = "data1")
public static class Data1ParameterizedType {
@PartitionKey
private int i;
@Column(codec = OptionalOfLong.class)
private Optional<Long> l;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
public Optional<Long> getL() {
return l;
}
public void setL(Optional<Long> l) {
this.l = l;
}
}
@Accessor
public interface Data1Accessor {
@Query("select * from data1 where i = :i")
Data1 get(@Param("i") CustomInt i);
@Query("update data1 set l = :l where i = :i")
void setL(@Param("i") int i,
@Param(value = "l", codec = CustomLong.Codec.class) CustomLong l);
}
@Accessor
public interface Data1AccessorNoDefaultConstructor {
@Query("update data1 set l = :l where i = :i")
void setL(@Param("i") int i,
@Param(value = "l", codec = NoDefaultConstructorCodec.class) long l);
}
@Accessor
public interface Data1AccessorInvalidCodecTypeMapping {
@Query("select * from data1 where i = :i")
Data1InvalidCodecTypeMapping get(@Param("i") int i);
@Query("update data1 set l = :l where i = :i")
void setL(@Param("i") int i,
@Param(value = "l", codec = OptionalOfString.class) Optional<Long> l);
}
@UDT(name = "holder")
public static class Holder {
private CustomInt i;
@Field(codec = CustomLong.Codec.class)
private CustomLong l;
public Holder() {
}
public Holder(int i, long l) {
this.i = new CustomInt(i);
this.l = new CustomLong(l);
}
public CustomInt getI() {
return i;
}
public void setI(CustomInt i) {
this.i = i;
}
public CustomLong getL() {
return l;
}
public void setL(CustomLong l) {
this.l = l;
}
}
@Table(name = "data2")
public static class Data2 {
@PartitionKey
private int i;
@Frozen
private Holder data;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
public Holder getData() {
return data;
}
public void setData(Holder data) {
this.data = data;
}
}
@Table(name = "data3")
public static class Data3 {
@PartitionKey
private int i;
@FrozenValue
private Map<CustomInt, Holder> data;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
public Map<CustomInt, Holder> getData() {
return data;
}
public void setData(Map<CustomInt, Holder> data) {
this.data = data;
}
}
public static class CustomInt {
public final int value;
public CustomInt(int value) {
this.value = value;
}
@Override
public boolean equals(Object other) {
if (other instanceof CustomInt) {
CustomInt that = (CustomInt) other;
return this.value == that.value;
}
return false;
}
@Override
public int hashCode() {
return value;
}
public static class Codec extends TypeCodec<CustomInt> {
public Codec() {
super(DataType.cint(), CustomInt.class);
}
@Override
public ByteBuffer serialize(CustomInt value, ProtocolVersion protocolVersion) throws InvalidTypeException {
return TypeCodec.cint().serialize(value.value, protocolVersion);
}
@Override
public CustomInt deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) throws InvalidTypeException {
Integer i = TypeCodec.cint().deserialize(bytes, protocolVersion);
return new CustomInt(i);
}
@Override
public CustomInt parse(String value) throws InvalidTypeException {
throw new UnsupportedOperationException();
}
@Override
public String format(CustomInt value) throws InvalidTypeException {
throw new UnsupportedOperationException();
}
}
}
public static class CustomLong {
public final long value;
public CustomLong(long value) {
this.value = value;
}
@Override
public boolean equals(Object other) {
if (other instanceof CustomLong) {
CustomLong that = (CustomLong) other;
return this.value == that.value;
}
return false;
}
@Override
public int hashCode() {
return (int) (value ^ (value >>> 32));
}
public static class Codec extends TypeCodec<CustomLong> {
public Codec() {
super(DataType.bigint(), CustomLong.class);
}
@Override
public ByteBuffer serialize(CustomLong value, ProtocolVersion protocolVersion) throws InvalidTypeException {
return TypeCodec.bigint().serialize(value.value, protocolVersion);
}
@Override
public CustomLong deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) throws InvalidTypeException {
Long l = TypeCodec.bigint().deserialize(bytes, protocolVersion);
return new CustomLong(l);
}
@Override
public CustomLong parse(String value) throws InvalidTypeException {
throw new UnsupportedOperationException();
}
@Override
public String format(CustomLong value) throws InvalidTypeException {
throw new UnsupportedOperationException();
}
}
}
public static class OptionalOfLong extends OptionalCodec<Long> {
private static final CodecRegistry registry = new CodecRegistry();
public OptionalOfLong() {
super(registry.codecFor(DataType.bigint(), Long.class));
}
}
public static class OptionalOfString extends OptionalCodec<String> {
private static final CodecRegistry registry = new CodecRegistry();
public OptionalOfString() {
super(registry.codecFor(DataType.text(), String.class));
}
}
/**
* This class is a copy of GuavaOptionalCodec declared in the extras module,
* to avoid circular dependencies between Maven modules.
*/
public static class OptionalCodec<T> extends MappingCodec<Optional<T>, T> {
private final Predicate<T> isAbsent;
public OptionalCodec(TypeCodec<T> codec) {
// @formatter:off
super(codec,
new TypeToken<Optional<T>>() {}.where(new TypeParameter<T>() {}, codec.getJavaType()));
// @formatter:on
this.isAbsent = new Predicate<T>() {
@Override
public boolean apply(T input) {
return input == null
|| input instanceof Collection && ((Collection) input).isEmpty()
|| input instanceof Map && ((Map) input).isEmpty();
}
};
}
@Override
protected Optional<T> deserialize(T value) {
return isAbsent(value) ? Optional.<T>absent() : Optional.fromNullable(value);
}
@Override
protected T serialize(Optional<T> value) {
return value.isPresent() ? value.get() : absentValue();
}
protected T absentValue() {
return null;
}
protected boolean isAbsent(T value) {
return isAbsent.apply(value);
}
}
private static class NoDefaultConstructorCodec extends TypeCodec<String> {
public NoDefaultConstructorCodec(DataType cqlType, Class<String> javaClass) {
super(cqlType, javaClass);
}
@Override
public ByteBuffer serialize(String value, ProtocolVersion protocolVersion) throws InvalidTypeException {
return null;
}
@Override
public String deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) throws InvalidTypeException {
return null;
}
@Override
public String parse(String value) throws InvalidTypeException {
return null;
}
@Override
public String format(String value) throws InvalidTypeException {
return null;
}
}
}