/*
* 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.CodecNotFoundException;
import com.datastax.driver.core.exceptions.InvalidQueryException;
import com.datastax.driver.core.querybuilder.BuiltStatement;
import com.datastax.driver.core.utils.CassandraVersion;
import com.datastax.driver.core.utils.MoreObjects;
import com.datastax.driver.core.utils.UUIDs;
import com.datastax.driver.mapping.annotations.*;
import com.google.common.collect.Maps;
import org.assertj.core.data.MapEntry;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.*;
import static com.datastax.driver.core.CreateCCM.TestMode.PER_METHOD;
import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.testng.Assert.*;
@SuppressWarnings({"unused", "WeakerAccess"})
@CassandraVersion("2.1.0")
@CreateCCM(PER_METHOD)
public class MapperUDTTest extends CCMTestsSupport {
@BeforeMethod(groups = "short")
public void createObjects() {
execute("CREATE TYPE address (street text, city text, \"ZIP_code\" int, phones set<text>)",
"CREATE TABLE users (user_id uuid PRIMARY KEY, name text, mainaddress frozen<address>, other_addresses map<text,frozen<address>>)");
}
@AfterMethod(groups = "short")
public void deleteObjects() {
execute("DROP TABLE IF EXISTS users",
"DROP TYPE IF EXISTS address");
}
@Table(name = "users",
readConsistency = "QUORUM",
writeConsistency = "QUORUM")
public static class User {
@PartitionKey
@Column(name = "user_id")
private UUID userId;
private String name;
@Frozen
private Address mainAddress;
@Column(name = "other_addresses")
@FrozenValue
private Map<String, Address> otherAddresses = Maps.newHashMap();
public User() {
}
public User(String name, Address address) {
this.userId = UUIDs.random();
this.name = name;
this.mainAddress = address;
this.otherAddresses = new HashMap<String, Address>();
}
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getMainAddress() {
return mainAddress;
}
public void setMainAddress(Address address) {
this.mainAddress = address;
}
public Map<String, Address> getOtherAddresses() {
return otherAddresses;
}
public void setOtherAddresses(Map<String, Address> otherAddresses) {
this.otherAddresses = otherAddresses;
}
public void addOtherAddress(String name, Address address) {
this.otherAddresses.put(name, address);
}
@Override
public boolean equals(Object other) {
if (this == other)
return true;
if (other instanceof User) {
User that = (User) other;
return MoreObjects.equal(this.userId, that.userId) &&
MoreObjects.equal(this.name, that.name) &&
MoreObjects.equal(this.mainAddress, that.mainAddress) &&
MoreObjects.equal(this.otherAddresses, that.otherAddresses);
}
return false;
}
@Override
public int hashCode() {
return MoreObjects.hashCode(this.userId, this.name, this.mainAddress, this.otherAddresses);
}
@Override
public String toString() {
return String.format("User(userId=%s, name=%s, mainAddress=%s, otherAddresses=%s)",
userId, name, mainAddress, otherAddresses);
}
}
@UDT(name = "address")
public static class Address {
// Dummy constant to test that static fields are properly ignored
public static final int FOO = 1;
@Field // not strictly required, but we want to check that the annotation works without a name
private String city;
// Declared out of order compared to the UDT definition, to make sure that we serialize fields in the correct order (JAVA-884)
private String street;
@Field(name = "ZIP_code", caseSensitive = true)
private int zipCode;
private Set<String> phones;
public Address() {
}
public Address(String street, String city, int zipCode, String... phones) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
this.phones = new HashSet<String>();
Collections.addAll(this.phones, phones);
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public int getZipCode() {
return zipCode;
}
public void setZipCode(int zipCode) {
this.zipCode = zipCode;
}
public Set<String> getPhones() {
return phones;
}
public void setPhones(Set<String> phones) {
this.phones = phones;
}
@Override
public boolean equals(Object other) {
if (this == other)
return true;
if (other instanceof Address) {
Address that = (Address) other;
return MoreObjects.equal(this.street, that.street) &&
MoreObjects.equal(this.city, that.city) &&
MoreObjects.equal(this.zipCode, that.zipCode) &&
MoreObjects.equal(this.phones, that.phones);
}
return false;
}
@Override
public int hashCode() {
return MoreObjects.hashCode(this.street, this.city, this.zipCode, this.phones);
}
@Override
public String toString() {
return String.format("Address(street=%s, city=%s, zip=%d, phones=%s)",
street, city, zipCode, phones);
}
}
@Accessor
public interface UserAccessor {
@Query("SELECT * FROM users WHERE user_id=:userId")
User getOne(@Param("userId") UUID userId);
@Query("UPDATE users SET other_addresses[:name]=:address WHERE user_id=:id")
ResultSet addAddress(@Param("id") UUID id, @Param("name") String addressName, @Param("address") Address address);
@Query("UPDATE users SET other_addresses=:addresses where user_id=:id")
ResultSet setOtherAddresses(@Param("id") UUID id, @Param("addresses") Map<String, Address> addresses);
@Query("SELECT * FROM users")
Result<User> getAll();
}
@Test(groups = "short")
public void testSimpleEntity() throws Exception {
Mapper<User> m = new MappingManager(session()).mapper(User.class);
User u1 = new User("Paul", new Address("12 4th Street", "Springfield", 12345, "12341343", "435423245"));
u1.addOtherAddress("work", new Address("5 Main Street", "Springfield", 12345, "23431342"));
m.save(u1);
assertEquals(m.get(u1.getUserId()), u1);
}
@Test(groups = "short")
public void should_handle_null_UDT_value() throws Exception {
Mapper<User> m = new MappingManager(session()).mapper(User.class);
User u1 = new User("Paul", null);
m.save(u1);
assertNull(m.get(u1.getUserId()).getMainAddress());
}
@Test(groups = "short")
public void testAccessor() throws Exception {
MappingManager manager = new MappingManager(session());
Mapper<User> m = new MappingManager(session()).mapper(User.class);
User u1 = new User("Paul", new Address("12 4th Street", "Springfield", 12345, "12341343", "435423245"));
m.save(u1);
UserAccessor userAccessor = manager.createAccessor(UserAccessor.class);
Address workAddress = new Address("5 Main Street", "Springfield", 12345, "23431342");
userAccessor.addAddress(u1.getUserId(), "work", workAddress);
User u2 = userAccessor.getOne(u1.getUserId());
assertEquals(workAddress, u2.getOtherAddresses().get("work"));
// Adding a null value should remove it from the list.
userAccessor.addAddress(u1.getUserId(), "work", null);
User u3 = userAccessor.getOne(u1.getUserId());
assertThat(u3.getOtherAddresses()).doesNotContainKey("work");
// Add a bunch of other addresses
Map<String, Address> otherAddresses = Maps.newHashMap();
otherAddresses.put("work", workAddress);
otherAddresses.put("cabin", new Address("42 Middle of Nowhere", "Lake of the Woods", 49553, "8675309"));
userAccessor.setOtherAddresses(u1.getUserId(), otherAddresses);
User u4 = userAccessor.getOne(u1.getUserId());
assertThat(u4.getOtherAddresses()).isEqualTo(otherAddresses);
// Nullify other addresses
userAccessor.setOtherAddresses(u1.getUserId(), null);
User u5 = userAccessor.getOne(u1.getUserId());
assertThat(u5.getOtherAddresses()).isEmpty();
// No argument call
Result<User> u = userAccessor.getAll();
assertEquals(u.one(), u5);
assertTrue(u.isExhausted());
}
@Test(groups = "short")
public void should_be_able_to_use_udtCodec_standalone() {
// Create a separate Cluster/Session to start with a CodecRegistry from scratch (so not already registered).
Cluster cluster = register(Cluster.builder()
.addContactPoints(getContactPoints())
.withPort(ccm().getBinaryPort())
.build());
CodecRegistry registry = cluster.getConfiguration().getCodecRegistry();
Session session = cluster.connect(keyspace);
UUID userId = UUIDs.random();
// Create a user.
session.execute("update users SET other_addresses['condo']={street: '101 Ocean Ln', city: 'Jacksonville, FL', \"ZIP_code\": 89898, phones: {'8675309'}} " +
" WHERE user_id=" + TypeCodec.uuid().format(userId));
session.execute("update users SET mainaddress={street: '42 Middle of Nowhere', city: 'Lake of the Woods', \"ZIP_code\": 49553, phones: {'8675039'}} " +
" WHERE user_Id=" + TypeCodec.uuid().format(userId));
// Get the user.
Row row = session.execute("select * from users where user_id=?", userId).one();
UDTValue udtValue = row.getUDTValue("mainaddress");
assertThat(udtValue.getString("street")).isEqualTo("42 Middle of Nowhere");
Object udtObject = row.getObject("mainaddress");
assertThat(udtObject).isEqualTo(udtValue);
// There shouldn't be a codec for address.
try {
assertThat(registry.codecFor(udtValue.getType(), Address.class));
fail("Didn't expect to find codec for udtType <-> Address");
} catch (CodecNotFoundException e) {
// expected.
}
// Expect a this_udt <-> UDTValue codec to exist. This is a pretty safe bet or else we wouldn't get
// this value back.
TypeCodec<UDTValue> udtCodec = registry.codecFor(udtValue.getType());
assertThat(udtCodec.getCqlType()).isEqualTo(udtValue.getType());
assertThat(udtCodec.getJavaType().getRawType()).isEqualTo(UDTValue.class);
// Retrieve codec for Address, if it can be mapped it will be created, if already registered it'll be used.
TypeCodec<Address> codec = new MappingManager(session).udtCodec(Address.class);
// The codec should be registered after we call udtCodec.
assertThat(registry.codecFor(udtValue.getType(), Address.class)).isEqualTo(codec);
// Should be able to retrieve as an Address.
Address mainAddress = row.get("mainaddress", Address.class);
assertThat(mainAddress.getCity()).isEqualTo("Lake of the Woods");
assertThat(mainAddress.getStreet()).isEqualTo("42 Middle of Nowhere");
assertThat(mainAddress.getZipCode()).isEqualTo(49553);
assertThat(mainAddress.getPhones()).containsExactly("8675039");
// Should be able to retrieve within a Map.
Address expectedOtherAddress = new Address();
expectedOtherAddress.setStreet("101 Ocean Ln");
expectedOtherAddress.setCity("Jacksonville, FL");
expectedOtherAddress.setZipCode(89898);
expectedOtherAddress.setPhones(Collections.singleton("8675309"));
Map<String, Address> otherAddresses = row.getMap("other_addresses", String.class, Address.class);
assertThat(otherAddresses).containsOnly(MapEntry.entry("condo", expectedOtherAddress));
}
/**
* Ensures that if a table is altered after a {@link Mapper} is created that it continues to work as long as
* the change is compatible. A new column is added to the table in this case, which is a compatible change.
* <p/>
* It also ensures that after the change is made that requesting a new {@link Mapper} returns a different instance
* instead of the existing one.
*
* @jira_ticket JAVA-1126
* @test_category object_mapper
*/
@Test(groups = "short")
public void should_work_normally_when_when_table_is_altered_but_remains_compatible() {
MappingManager manager = new MappingManager(session());
User expected = new User("Paul", new Address("12 4th Street", "Springfield", 12345, "12341343", "435423245"));
Mapper<User> m = manager.mapper(User.class);
Mapper<User> m1 = manager.mapper(User.class);
assertThat(m1).isSameAs(m);
session().execute("ALTER TABLE users ADD foo text");
m.save(expected);
User retrieved = m.get(expected.getUserId());
assertThat(retrieved).isEqualTo(expected);
// Mapper should be recreated since table changed.
Mapper<User> m2 = manager.mapper(User.class);
assertThat(m2).isNotSameAs(m);
}
/**
* Ensures that if a UDT is altered after a {@link Mapper} is created that has a UDT that it continues to work as
* long as the change is compatible. A new field is added to the table in this case, which is a compatible change.
* <p/>
* It also ensures that after the change is made that requesting a new {@link TypeCodec} for that UDT and a new
* {@link Mapper} for a table using that UDT returns different instances instead of the existing ones.
*
* @jira_ticket JAVA-1126
* @test_category object_mapper
*/
@Test(groups = "short")
public void should_work_normally_when_udt_is_altered_but_remains_compatible() {
MappingManager manager = new MappingManager(session());
User expected = new User("Paul", new Address("12 4th Street", "Springfield", 12345, "12341343", "435423245"));
Mapper<User> m = manager.mapper(User.class);
TypeCodec c = manager.udtCodec(Address.class);
TypeCodec c1 = manager.udtCodec(Address.class);
assertThat(c1).isSameAs(c);
session().execute("ALTER TYPE address ADD foo text");
m.save(expected);
User retrieved = m.get(expected.getUserId());
assertThat(retrieved).isEqualTo(expected);
// Codec should be recreated when requested since UDT and thus the table changed.
Mapper m2 = manager.mapper(User.class);
assertThat(m2).isNotSameAs(m);
// Codec should be recreated when requested since UDT changed.
TypeCodec c2 = manager.udtCodec(Address.class);
assertThat(c2).isNotSameAs(c);
}
/**
* Ensures that if a table is dropped after a {@link Mapper} is created that the {@link Mapper} can no longer
* successfully make queries and that attempting to create a new {@link Mapper} throws an
* {@link IllegalArgumentException}.
* <p/>
* It also ensures that requesting a {@link Mapper} fails as the previous {@link Mapper} was evicted and a new
* one cannot be created as the table has been dropped.
*
* @jira_ticket JAVA-1126
* @test_category object_mapper
*/
@Test(groups = "short")
public void should_throw_error_when_table_is_dropped() {
User user = new User("Paul", new Address("12 4th Street", "Springfield", 12345, "12341343", "435423245"));
MappingManager manager = new MappingManager(session());
Mapper<User> mapper = manager.mapper(User.class);
session().execute("DROP TABLE users");
// usage of stale mapper
try {
mapper.save(user);
fail("Expected InvalidQueryException");
} catch (InvalidQueryException e) {
assertThat(e.getMessage()).contains("unconfigured", "users");
}
try {
mapper.get(user.getUserId());
fail("Expected InvalidQueryException");
} catch (InvalidQueryException e) {
assertThat(e.getMessage()).contains("unconfigured", "users");
}
// trying to use a new mapper
try {
manager.mapper(User.class);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage()).isEqualTo("Table or materialized view users does not exist in keyspace \"" + keyspace + "\"");
}
}
/**
* Ensures that if a table is altered after a {@link Mapper} is created that it no longer continues to work if
* the change is not compatible. A column is dropped from the table in this case, which is an incompatible change.
* <p/>
* It also ensures that requesting a {@link Mapper} fails as the previous {@link Mapper} was evicted and a new
* one cannot be created as a declared column is missing from the schema but is present in the class definition.
*
* @jira_ticket JAVA-1126
* @test_category object_mapper
*/
@Test(groups = "short")
public void should_throw_error_when_table_is_altered_and_is_not_compatible_anymore() {
User user = new User("Paul", new Address("12 4th Street", "Springfield", 12345, "12341343", "435423245"));
MappingManager manager = new MappingManager(session());
Mapper<User> mapper = manager.mapper(User.class);
session().execute("ALTER TABLE users DROP mainaddress");
// usage of stale mapper
try {
mapper.save(user);
fail("Expected InvalidQueryException");
} catch (InvalidQueryException e) {
// Error message varies by C* version.
assertThat(e.getMessage()).isIn("Unknown identifier mainaddress", "Undefined column name mainaddress");
}
try {
mapper.get(user.getUserId());
fail("Expected InvalidQueryException");
} catch (InvalidQueryException e) {
// Error message varies by C* version.
assertThat(e.getMessage()).isIn("Undefined name mainaddress in selection clause", "Undefined column name mainaddress");
}
// trying to use a new mapper
try {
manager.mapper(User.class);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage()).isIn(String.format("Column mainaddress does not exist in table \"%s\".users", keyspace));
}
}
/**
* Ensures that if a UDT is altered after a {@link Mapper} is created that it no longer continues to work if
* the change made to the UDT is not compatible. A field is renamed in the UDT in this case, which is not a
* compatible change.
* <p/>
* It also ensures that requesting a {@link Mapper} fails as the previous {@link Mapper} was evicted and a new
* one cannot be created as a declared field is missing from the schema but is present in the class definition.
* <p/>
* Also verifies that a {@link TypeCodec} cannot be requested for the UDT as well.
*
* @jira_ticket JAVA-1126
* @test_category object_mapper
*/
@Test(groups = "short")
public void should_throw_error_when_udt_is_altered_and_is_not_compatible_anymore() {
User user = new User("Paul", new Address("12 4th Street", "Springfield", 12345, "12341343", "435423245"));
MappingManager manager = new MappingManager(session());
Mapper<User> mapper = manager.mapper(User.class);
session().execute("ALTER TYPE address RENAME \"ZIP_code\" to zip_code");
// usage of stale mapper
try {
mapper.save(user);
fail("Expected CodecNotFoundException");
} catch (CodecNotFoundException e) {
// ok, codec could not be created
}
// insert manually to be able to test retrieval
session().execute("INSERT INTO users (user_id, name, mainaddress) VALUES (?, 'Paul', { street : '12 4th Street', zip_code : 12345 })", user.getUserId());
try {
mapper.get(user.getUserId());
fail("Expected CodecNotFoundException");
} catch (CodecNotFoundException e) {
// ok, codec could not be created
}
// trying to use a new mapper
try {
manager.mapper(User.class);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage()).isEqualTo(String.format("Field \"ZIP_code\" does not exist in type \"%s\".address", keyspace));
}
try {
manager.udtCodec(Address.class);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
assertThat(e.getMessage()).isEqualTo(String.format("Field \"ZIP_code\" does not exist in type \"%s\".address", keyspace));
}
}
@UDT(name = "address")
public static class AddressUnknownField {
private String city;
private String street;
@Field(name = "ZIP_code", caseSensitive = true)
private int zipCode;
private Set<String> phones;
public String province;
public AddressUnknownField() {
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public int getZipCode() {
return zipCode;
}
public void setZipCode(int zipCode) {
this.zipCode = zipCode;
}
public Set<String> getPhones() {
return phones;
}
public void setPhones(Set<String> phones) {
this.phones = phones;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
}
/**
* Ensures that when attempting to create a {@link TypeCodec} from a class that has a field that does not exist in
* the UDT that an {@link IllegalArgumentException} is thrown.
*
* @jira_ticket JAVA-1126
* @test_category object_mapper
*/
@Test(groups = "short", expectedExceptions = IllegalArgumentException.class)
public void should_fail_to_create_codec_if_class_has_field_not_in_udt() {
MappingManager manager = new MappingManager(session());
manager.getUDTCodec(AddressUnknownField.class);
}
@UDT(name = "nonexistent")
public static class NonExistentUDT {
public String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
/**
* Ensures that when attempting to create a {@link TypeCodec} from a class that has a {@link UDT} annotation with
* a name that doesn't exist in the current keyspace that an {@link IllegalArgumentException} is thrown.
*
* @jira_ticket JAVA-1126
* @test_category object_mapper
*/
@Test(groups = "short", expectedExceptions = IllegalArgumentException.class)
public void should_fail_to_create_codec_if_udt_does_not_exist() {
MappingManager manager = new MappingManager(session());
manager.getUDTCodec(NonExistentUDT.class);
}
@Table(name = "users")
public static class UserWithAddressUnknownField {
@PartitionKey
@Column(name = "user_id")
private UUID userId;
private String name;
@Frozen
private AddressUnknownField mainAddress;
@Column(name = "other_addresses")
@FrozenValue
private Map<String, Address> otherAddresses = Maps.newHashMap();
public UserWithAddressUnknownField() {
}
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public AddressUnknownField getMainAddress() {
return mainAddress;
}
public void setMainAddress(AddressUnknownField address) {
this.mainAddress = address;
}
public Map<String, Address> getOtherAddresses() {
return otherAddresses;
}
public void setOtherAddresses(Map<String, Address> otherAddresses) {
this.otherAddresses = otherAddresses;
}
public void addOtherAddress(String name, Address address) {
this.otherAddresses.put(name, address);
}
}
/**
* Ensures that when attempting to create a {@link Mapper} from a class that has a field that is a class mapping to
* a user type that has a field that does not exist in that UDT that an {@link IllegalArgumentException} is thrown.
*
* @jira_ticket JAVA-1126
* @test_category object_mapper
*/
@Test(groups = "short", expectedExceptions = IllegalArgumentException.class)
public void should_fail_to_create_mapper_if_class_has_udt_field_class_that_has_field_not_in_udt() {
MappingManager manager = new MappingManager(session());
manager.mapper(UserWithAddressUnknownField.class);
}
/**
* Ensures that MappedUDTCodec is able to properly format UDTs when printing the query string of a BuiltStatement.
*
* @jira_ticket JAVA-1272
* @test_category object_mapper
*/
@Test(groups = "short")
public void should_format_mapped_udt() throws Exception {
MappingManager manager = new MappingManager(session());
UUID uuid = UUIDs.random();
BuiltStatement update =
update("users")
.with(set("mainaddress", new Address("12 4th Street", "Springfield", 12345, "12341343", "435423245")))
.where(eq("user_id", uuid));
CodecRegistry codecRegistry = cluster().getConfiguration().getCodecRegistry();
codecRegistry.register(manager.udtCodec(Address.class));
String queryString = update.getQueryString(codecRegistry);
assertThat(queryString)
.isEqualTo("UPDATE users SET mainaddress=? WHERE user_id=?;");
update.setForceNoValues(true);
queryString = update.getQueryString(codecRegistry);
assertThat(queryString).isEqualTo(
"UPDATE users " +
"SET mainaddress={street:'12 4th Street',city:'Springfield',\"ZIP_code\":12345,phones:{'435423245','12341343'}} " +
"WHERE user_id=" + uuid + ";");
// check that the query string is valid
session().execute(queryString);
}
/**
* Ensures that MappedUDTCodec is able to properly parse UDTs.
*
* @jira_ticket JAVA-1272
* @test_category object_mapper
*/
@Test(groups = "short")
public void should_parse_mapped_udt() throws Exception {
MappingManager manager = new MappingManager(session());
TypeCodec<Address> codec = manager.udtCodec(Address.class);
Address actual = codec.parse("{street:'12 4th Street',city:'Springfield',\"ZIP_code\":12345,phones:{'435423245','12341343'}}");
assertThat(actual).isEqualTo(new Address("12 4th Street", "Springfield", 12345, "12341343", "435423245"));
}
}