From bac1998d6a17f4f9953bc4a976ab76b4534dbe70 Mon Sep 17 00:00:00 2001 From: Shicong Huang Date: Thu, 2 Apr 2020 16:43:08 -0400 Subject: [PATCH] Auto-apply JPA converters for map type (#520) * Add map converter * Delete old map usertype * Refactor bind * Change to use map entry * Use Map.Entry --- .../registry/model/registrar/Registrar.java | 2 - .../persistence/NomulusPostgreSQLDialect.java | 5 +- .../converter/CurrencyToBillingConverter.java | 43 +++++ .../CurrencyToBillingMapUserType.java | 54 ------ .../persistence/converter/MapUserType.java | 73 -------- .../converter/MutableUserType.java | 63 ------- .../converter/StringMapConverterBase.java | 52 +++++ .../converter/StringMapDescriptor.java | 177 ++++++++++++++++++ .../main/resources/META-INF/persistence.xml | 1 + ...=> CidrAddressBlockListConverterTest.java} | 2 +- ...va => CurrencyToBillingConverterTest.java} | 6 +- ...t.java => StringMapConverterBaseTest.java} | 75 +++++--- 12 files changed, 333 insertions(+), 220 deletions(-) create mode 100644 core/src/main/java/google/registry/persistence/converter/CurrencyToBillingConverter.java delete mode 100644 core/src/main/java/google/registry/persistence/converter/CurrencyToBillingMapUserType.java delete mode 100644 core/src/main/java/google/registry/persistence/converter/MapUserType.java delete mode 100644 core/src/main/java/google/registry/persistence/converter/MutableUserType.java create mode 100644 core/src/main/java/google/registry/persistence/converter/StringMapConverterBase.java create mode 100644 core/src/main/java/google/registry/persistence/converter/StringMapDescriptor.java rename core/src/test/java/google/registry/persistence/converter/{CidrAddressBlockListUserTypeTest.java => CidrAddressBlockListConverterTest.java} (98%) rename core/src/test/java/google/registry/persistence/converter/{CurrencyToBillingMapUserTypeTest.java => CurrencyToBillingConverterTest.java} (92%) rename core/src/test/java/google/registry/persistence/converter/{MapUserTypeTest.java => StringMapConverterBaseTest.java} (69%) diff --git a/core/src/main/java/google/registry/model/registrar/Registrar.java b/core/src/main/java/google/registry/model/registrar/Registrar.java index c364ee609..6d6d191ee 100644 --- a/core/src/main/java/google/registry/model/registrar/Registrar.java +++ b/core/src/main/java/google/registry/model/registrar/Registrar.java @@ -392,8 +392,6 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable */ @Nullable @Mapify(CurrencyMapper.class) - @org.hibernate.annotations.Type( - type = "google.registry.persistence.converter.CurrencyToBillingMapUserType") Map billingAccountMap; /** A billing account entry for this registrar, consisting of a currency and an account Id. */ diff --git a/core/src/main/java/google/registry/persistence/NomulusPostgreSQLDialect.java b/core/src/main/java/google/registry/persistence/NomulusPostgreSQLDialect.java index 3e209473c..6f60d7c38 100644 --- a/core/src/main/java/google/registry/persistence/NomulusPostgreSQLDialect.java +++ b/core/src/main/java/google/registry/persistence/NomulusPostgreSQLDialect.java @@ -14,6 +14,7 @@ package google.registry.persistence; import google.registry.persistence.converter.StringCollectionDescriptor; +import google.registry.persistence.converter.StringMapDescriptor; import java.sql.Types; import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.PostgreSQL95Dialect; @@ -26,7 +27,7 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect { registerColumnType(Types.VARCHAR, "text"); registerColumnType(Types.TIMESTAMP_WITH_TIMEZONE, "timestamptz"); registerColumnType(Types.TIMESTAMP, "timestamptz"); - registerColumnType(Types.OTHER, "hstore"); + registerColumnType(StringMapDescriptor.COLUMN_TYPE, StringMapDescriptor.COLUMN_NAME); registerColumnType( StringCollectionDescriptor.COLUMN_TYPE, StringCollectionDescriptor.COLUMN_DDL_NAME); } @@ -37,5 +38,7 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect { super.contributeTypes(typeContributions, serviceRegistry); typeContributions.contributeJavaTypeDescriptor(StringCollectionDescriptor.getInstance()); typeContributions.contributeSqlTypeDescriptor(StringCollectionDescriptor.getInstance()); + typeContributions.contributeJavaTypeDescriptor(StringMapDescriptor.getInstance()); + typeContributions.contributeSqlTypeDescriptor(StringMapDescriptor.getInstance()); } } diff --git a/core/src/main/java/google/registry/persistence/converter/CurrencyToBillingConverter.java b/core/src/main/java/google/registry/persistence/converter/CurrencyToBillingConverter.java new file mode 100644 index 000000000..72a98f09d --- /dev/null +++ b/core/src/main/java/google/registry/persistence/converter/CurrencyToBillingConverter.java @@ -0,0 +1,43 @@ +// Copyright 2020 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.persistence.converter; + +import static google.registry.model.registrar.Registrar.BillingAccountEntry; + +import com.google.common.collect.Maps; +import java.util.Map; +import javax.persistence.Converter; +import org.joda.money.CurrencyUnit; + +/** JPA converter for storing/retrieving {@link Map } objects. */ +@Converter(autoApply = true) +public class CurrencyToBillingConverter + extends StringMapConverterBase { + + @Override + Map.Entry convertToDatabaseMapEntry( + Map.Entry entry) { + return Maps.immutableEntry(entry.getKey().getCode(), entry.getValue().getAccountId()); + } + + @Override + Map.Entry convertToEntityMapEntry( + Map.Entry entry) { + CurrencyUnit currencyUnit = CurrencyUnit.of(entry.getKey()); + BillingAccountEntry billingAccountEntry = + new BillingAccountEntry(currencyUnit, entry.getValue()); + return Maps.immutableEntry(currencyUnit, billingAccountEntry); + } +} diff --git a/core/src/main/java/google/registry/persistence/converter/CurrencyToBillingMapUserType.java b/core/src/main/java/google/registry/persistence/converter/CurrencyToBillingMapUserType.java deleted file mode 100644 index 1c03abbb1..000000000 --- a/core/src/main/java/google/registry/persistence/converter/CurrencyToBillingMapUserType.java +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2020 The Nomulus Authors. All Rights Reserved. -// -// 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 google.registry.persistence.converter; - -import static com.google.common.collect.ImmutableMap.toImmutableMap; - -import google.registry.model.registrar.Registrar.BillingAccountEntry; -import java.util.Map; -import org.hibernate.usertype.UserType; -import org.joda.money.CurrencyUnit; - -/** - * A custom {@link UserType} for storing/retrieving {@link Map} - * objects. - */ -public class CurrencyToBillingMapUserType extends MapUserType { - - @Override - public Object toEntityTypeMap(Map map) { - return map == null - ? null - : map.entrySet().stream() - .collect( - toImmutableMap( - entry -> CurrencyUnit.of(entry.getKey()), - entry -> - new BillingAccountEntry( - CurrencyUnit.of(entry.getKey()), entry.getValue()))); - } - - @Override - public Map toDbSupportedMap(Object map) { - return map == null - ? null - : ((Map) map) - .entrySet().stream() - .collect( - toImmutableMap( - entry -> entry.getKey().getCode(), - entry -> entry.getValue().getAccountId())); - } -} diff --git a/core/src/main/java/google/registry/persistence/converter/MapUserType.java b/core/src/main/java/google/registry/persistence/converter/MapUserType.java deleted file mode 100644 index 4b1dd3a79..000000000 --- a/core/src/main/java/google/registry/persistence/converter/MapUserType.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2020 The Nomulus Authors. All Rights Reserved. -// -// 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 google.registry.persistence.converter; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Types; -import java.util.Map; -import org.hibernate.HibernateException; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.usertype.UserType; - -/** - * A custom {@link UserType} used to convert a Java {@link Map} to/from PostgreSQL - * hstore type. Per this doc, as - * hstore keys and values are simply text strings, the type of key and value in the Java map has to - * be {@link String} as well. - */ -public class MapUserType extends MutableUserType { - - @Override - public int[] sqlTypes() { - return new int[] {Types.OTHER}; - } - - @Override - public Class returnedClass() { - return Map.class; - } - - @Override - public Object nullSafeGet( - ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) - throws HibernateException, SQLException { - return toEntityTypeMap((Map) rs.getObject(names[0])); - } - - @Override - public void nullSafeSet( - PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) - throws HibernateException, SQLException { - st.setObject(index, toDbSupportedMap(value)); - } - - /** - * Subclass can override this method to convert the {@link Map} to a {@link Map} - * of specific type defined in the entity class. - */ - public Object toEntityTypeMap(Map map) { - return map; - } - - /** - * Subclass can override this method to convert the {@link Map} of specific type to a {@link - * Map} that can be stored in the hstore type column. - */ - public Map toDbSupportedMap(Object map) { - return (Map) map; - } -} diff --git a/core/src/main/java/google/registry/persistence/converter/MutableUserType.java b/core/src/main/java/google/registry/persistence/converter/MutableUserType.java deleted file mode 100644 index 519b81722..000000000 --- a/core/src/main/java/google/registry/persistence/converter/MutableUserType.java +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2020 The Nomulus Authors. All Rights Reserved. -// -// 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 google.registry.persistence.converter; - -import java.io.Serializable; -import java.util.Objects; -import org.hibernate.HibernateException; -import org.hibernate.usertype.UserType; - -/** - * An abstract class represents a mutable Hibernate user type which implements related methods - * defined in {@link UserType}. - */ -public abstract class MutableUserType implements UserType { - - @Override - public boolean equals(Object x, Object y) throws HibernateException { - return Objects.equals(x, y); - } - - @Override - public int hashCode(Object x) throws HibernateException { - return x == null ? 0 : x.hashCode(); - } - - // TODO(b/147489651): Investigate how to properly implement the below methods. - @Override - public Object deepCopy(Object value) throws HibernateException { - return value; - } - - @Override - public boolean isMutable() { - return false; - } - - @Override - public Serializable disassemble(Object value) throws HibernateException { - return (Serializable) value; - } - - @Override - public Object assemble(Serializable cached, Object owner) throws HibernateException { - return cached; - } - - @Override - public Object replace(Object original, Object target, Object owner) throws HibernateException { - return original; - } -} diff --git a/core/src/main/java/google/registry/persistence/converter/StringMapConverterBase.java b/core/src/main/java/google/registry/persistence/converter/StringMapConverterBase.java new file mode 100644 index 000000000..eea274040 --- /dev/null +++ b/core/src/main/java/google/registry/persistence/converter/StringMapConverterBase.java @@ -0,0 +1,52 @@ +// Copyright 2020 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.persistence.converter; + +import static com.google.common.collect.ImmutableMap.toImmutableMap; + +import google.registry.persistence.converter.StringMapDescriptor.StringMap; +import java.util.Map; +import javax.persistence.AttributeConverter; + +/** + * Base JPA converter for {@link Map} objects that are stored in a column with data type of hstore + * in the database. + */ +public abstract class StringMapConverterBase + implements AttributeConverter, StringMap> { + + abstract Map.Entry convertToDatabaseMapEntry(Map.Entry entry); + + abstract Map.Entry convertToEntityMapEntry(Map.Entry entry); + + @Override + public StringMap convertToDatabaseColumn(Map attribute) { + return attribute == null + ? null + : StringMap.create( + attribute.entrySet().stream() + .map(this::convertToDatabaseMapEntry) + .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))); + } + + @Override + public Map convertToEntityAttribute(StringMap dbData) { + return dbData == null + ? null + : dbData.getMap().entrySet().stream() + .map(this::convertToEntityMapEntry) + .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} diff --git a/core/src/main/java/google/registry/persistence/converter/StringMapDescriptor.java b/core/src/main/java/google/registry/persistence/converter/StringMapDescriptor.java new file mode 100644 index 000000000..8bae07999 --- /dev/null +++ b/core/src/main/java/google/registry/persistence/converter/StringMapDescriptor.java @@ -0,0 +1,177 @@ +// Copyright 2020 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.persistence.converter; + +import static google.registry.persistence.converter.StringMapDescriptor.StringMap; + +import com.google.common.collect.ImmutableMap; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Map; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractTypeDescriptor; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.descriptor.spi.JdbcRecommendedSqlTypeMappingContext; +import org.hibernate.type.descriptor.sql.BasicBinder; +import org.hibernate.type.descriptor.sql.BasicExtractor; +import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; + +/** + * The {@link JavaTypeDescriptor} and {@link SqlTypeDescriptor} for {@link StringMap}. + * + *

A {@link StringMap} object is a simple wrapper for a {@link Map } which can be + * stored in a column with data type of hstore in the database. The {@link JavaTypeDescriptor} and + * {@link SqlTypeDescriptor} is used by JPA/Hibernate to map between the map and hstore which is the + * actual type that JDBC uses to read from and write to the database. + * + * @see JPA + * 2.1 AttributeConverters + * @see hstore + */ +public class StringMapDescriptor extends AbstractTypeDescriptor + implements SqlTypeDescriptor { + public static final int COLUMN_TYPE = Types.OTHER; + public static final String COLUMN_NAME = "hstore"; + private static final StringMapDescriptor INSTANCE = new StringMapDescriptor(); + + protected StringMapDescriptor() { + super(StringMap.class); + } + + public static StringMapDescriptor getInstance() { + return INSTANCE; + } + + @Override + public StringMap fromString(String string) { + throw new UnsupportedOperationException( + "Constructing StringMapDescriptor from string is not allowed"); + } + + @Override + public X unwrap(StringMap value, Class type, WrapperOptions options) { + if (value == null) { + return null; + } + if (Map.class.isAssignableFrom(type)) { + return (X) value.getMap(); + } + throw unknownUnwrap(type); + } + + @Override + public StringMap wrap(X value, WrapperOptions options) { + if (value == null) { + return null; + } + if (value instanceof Map) { + return StringMap.create((Map) value); + } + throw unknownWrap(value.getClass()); + } + + @Override + public SqlTypeDescriptor getJdbcRecommendedSqlType(JdbcRecommendedSqlTypeMappingContext context) { + return this; + } + + @Override + public int getSqlType() { + return COLUMN_TYPE; + } + + @Override + public boolean canBeRemapped() { + return false; + } + + @Override + public ValueBinder getBinder(JavaTypeDescriptor javaTypeDescriptor) { + return new BasicBinder(javaTypeDescriptor, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + st.setObject(index, getStringMap(value)); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + st.setObject(name, getStringMap(value)); + } + + private Map getStringMap(X value) { + if (value == null) { + return null; + } + if (value instanceof StringMap) { + return ((StringMap) value).getMap(); + } else { + throw new UnsupportedOperationException( + String.format( + "Binding type %s is not supported by StringMapDescriptor", + value.getClass().getName())); + } + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaTypeDescriptor javaTypeDescriptor) { + return new BasicExtractor(javaTypeDescriptor, this) { + @Override + protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { + return javaTypeDescriptor.wrap(rs.getObject(name), options); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + return javaTypeDescriptor.wrap(statement.getObject(index), options); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + return javaTypeDescriptor.wrap(statement.getObject(name), options); + } + }; + } + + /** A simple wrapper class for {@link Map}. */ + public static class StringMap { + private Map map; + + private StringMap(Map map) { + this.map = map; + } + + /** Constructs an instance of {@link StringMap} from the given map. */ + public static StringMap create(Map map) { + return new StringMap(ImmutableMap.copyOf(map)); + } + + /** Returns the underlying {@link Map} object. */ + public Map getMap() { + return map; + } + } +} diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml index 392aef01c..d73b3ed8e 100644 --- a/core/src/main/resources/META-INF/persistence.xml +++ b/core/src/main/resources/META-INF/persistence.xml @@ -36,6 +36,7 @@ google.registry.persistence.converter.BloomFilterConverter google.registry.persistence.converter.CidrAddressBlockListConverter google.registry.persistence.converter.CreateAutoTimestampConverter + google.registry.persistence.converter.CurrencyToBillingConverter google.registry.persistence.converter.CurrencyUnitConverter google.registry.persistence.converter.DateTimeConverter google.registry.persistence.converter.DurationConverter diff --git a/core/src/test/java/google/registry/persistence/converter/CidrAddressBlockListUserTypeTest.java b/core/src/test/java/google/registry/persistence/converter/CidrAddressBlockListConverterTest.java similarity index 98% rename from core/src/test/java/google/registry/persistence/converter/CidrAddressBlockListUserTypeTest.java rename to core/src/test/java/google/registry/persistence/converter/CidrAddressBlockListConverterTest.java index 84c09fa5a..7287d58df 100644 --- a/core/src/test/java/google/registry/persistence/converter/CidrAddressBlockListUserTypeTest.java +++ b/core/src/test/java/google/registry/persistence/converter/CidrAddressBlockListConverterTest.java @@ -32,7 +32,7 @@ import org.junit.runners.JUnit4; /** Unit tests for {@link CidrAddressBlockListConverter}. */ @RunWith(JUnit4.class) -public class CidrAddressBlockListUserTypeTest { +public class CidrAddressBlockListConverterTest { @Rule public final JpaUnitTestRule jpaRule = new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule(); diff --git a/core/src/test/java/google/registry/persistence/converter/CurrencyToBillingMapUserTypeTest.java b/core/src/test/java/google/registry/persistence/converter/CurrencyToBillingConverterTest.java similarity index 92% rename from core/src/test/java/google/registry/persistence/converter/CurrencyToBillingMapUserTypeTest.java rename to core/src/test/java/google/registry/persistence/converter/CurrencyToBillingConverterTest.java index 50339faa1..1682c41ed 100644 --- a/core/src/test/java/google/registry/persistence/converter/CurrencyToBillingMapUserTypeTest.java +++ b/core/src/test/java/google/registry/persistence/converter/CurrencyToBillingConverterTest.java @@ -25,16 +25,15 @@ import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule; import java.util.Map; import javax.persistence.Entity; import javax.persistence.Id; -import org.hibernate.annotations.Type; import org.joda.money.CurrencyUnit; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** Unit tests for {@link CurrencyToBillingMapUserType}. */ +/** Unit tests for {@link CurrencyToBillingConverter}. */ @RunWith(JUnit4.class) -public class CurrencyToBillingMapUserTypeTest { +public class CurrencyToBillingConverterTest { @Rule public final JpaUnitTestRule jpaRule = new JpaTestRules.Builder() @@ -62,7 +61,6 @@ public class CurrencyToBillingMapUserTypeTest { @Id String name = "id"; - @Type(type = "google.registry.persistence.converter.CurrencyToBillingMapUserType") Map currencyToBilling; private TestEntity() {} diff --git a/core/src/test/java/google/registry/persistence/converter/MapUserTypeTest.java b/core/src/test/java/google/registry/persistence/converter/StringMapConverterBaseTest.java similarity index 69% rename from core/src/test/java/google/registry/persistence/converter/MapUserTypeTest.java rename to core/src/test/java/google/registry/persistence/converter/StringMapConverterBaseTest.java index 19b35560f..61749e952 100644 --- a/core/src/test/java/google/registry/persistence/converter/MapUserTypeTest.java +++ b/core/src/test/java/google/registry/persistence/converter/StringMapConverterBaseTest.java @@ -19,54 +19,56 @@ import static google.registry.persistence.transaction.TransactionManagerFactory. import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; import google.registry.model.ImmutableObject; import google.registry.persistence.transaction.JpaTestRules; -import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule; import java.util.Map; +import javax.persistence.Converter; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.NoResultException; -import org.hibernate.annotations.Type; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** Unit tests for {@link MapUserType}. */ +/** Unit tests for {@link StringMapConverterBase}. */ @RunWith(JUnit4.class) -public class MapUserTypeTest { - - // Reusing production script sql/flyway/V14__load_extension_for_hstore.sql, which loads the - // hstore extension but nothing else. +public class StringMapConverterBaseTest { @Rule - public final JpaUnitTestRule jpaRule = + public final JpaTestRules.JpaUnitTestRule jpaRule = new JpaTestRules.Builder() .withInitScript("sql/flyway/V14__load_extension_for_hstore.sql") - .withEntityClass(TestEntity.class) + .withEntityClass(TestStringMapConverter.class, TestEntity.class) .buildUnitTestRule(); + private static final ImmutableMap MAP = + ImmutableMap.of( + new Key("key1"), new Value("value1"), + new Key("key2"), new Value("value2"), + new Key("key3"), new Value("value3")); + @Test public void roundTripConversion_returnsSameMap() { - Map map = ImmutableMap.of("key1", "value1", "key2", "value2"); - TestEntity testEntity = new TestEntity(map); + TestEntity testEntity = new TestEntity(MAP); jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); TestEntity persisted = jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); - assertThat(persisted.map).containsExactly("key1", "value1", "key2", "value2"); + assertThat(persisted.map).containsExactlyEntriesIn(MAP); } @Test - public void testMerge_succeeds() { - Map map = ImmutableMap.of("key1", "value1", "key2", "value2"); - TestEntity testEntity = new TestEntity(map); + public void testUpdateColumn_succeeds() { + TestEntity testEntity = new TestEntity(MAP); jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); TestEntity persisted = jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); - persisted.map = ImmutableMap.of("key3", "value3"); + assertThat(persisted.map).containsExactlyEntriesIn(MAP); + persisted.map = ImmutableMap.of(new Key("key4"), new Value("value4")); jpaTm().transact(() -> jpaTm().getEntityManager().merge(persisted)); TestEntity updated = jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); - assertThat(updated.map).containsExactly("key3", "value3"); + assertThat(updated.map).containsExactly(new Key("key4"), new Value("value4")); } @Test @@ -79,7 +81,7 @@ public class MapUserTypeTest { } @Test - public void testEmptyCollection_writesAndReadsEmptyCollectionSuccessfully() { + public void testEmptyMap_writesAndReadsEmptyCollectionSuccessfully() { TestEntity testEntity = new TestEntity(ImmutableMap.of()); jpaTm().transact(() -> jpaTm().getEntityManager().persist(testEntity)); TestEntity persisted = @@ -88,7 +90,7 @@ public class MapUserTypeTest { } @Test - public void testNativeQuery_succeeds() throws Exception { + public void testNativeQuery_succeeds() { executeNativeQuery( "INSERT INTO \"TestEntity\" (name, map) VALUES ('id', 'key1=>value1, key2=>value2')"); @@ -126,17 +128,46 @@ public class MapUserTypeTest { .transact(() -> jpaTm().getEntityManager().createNativeQuery(sql).executeUpdate()); } + private static class Key extends ImmutableObject { + private String key; + + private Key(String key) { + this.key = key; + } + } + + private static class Value extends ImmutableObject { + private String value; + + private Value(String value) { + this.value = value; + } + } + + @Converter(autoApply = true) + private static class TestStringMapConverter extends StringMapConverterBase { + + @Override + Map.Entry convertToDatabaseMapEntry(Map.Entry entry) { + return Maps.immutableEntry(entry.getKey().key, entry.getValue().value); + } + + @Override + Map.Entry convertToEntityMapEntry(Map.Entry entry) { + return Maps.immutableEntry(new Key(entry.getKey()), new Value(entry.getValue())); + } + } + @Entity(name = "TestEntity") // Override entity name to avoid the nested class reference. private static class TestEntity extends ImmutableObject { @Id String name = "id"; - @Type(type = "google.registry.persistence.converter.MapUserType") - Map map; + Map map; private TestEntity() {} - private TestEntity(Map map) { + private TestEntity(Map map) { this.map = map; } }