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