| /* |
| * 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; |
| } |
| |
| } |
| |
| } |