Auto-apply JPA converters for collection type (#469)

* Auto-apply JPA converters for collection type

* Extract common logic to a base class

* Remove extra lines

* Rebase on master
This commit is contained in:
Shicong Huang 2020-02-10 10:33:43 -05:00 committed by GitHub
parent 736f788eea
commit 594ce30122
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 408 additions and 261 deletions

View file

@ -53,7 +53,6 @@ import java.util.concurrent.ExecutionException;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.MappedSuperclass; import javax.persistence.MappedSuperclass;
import javax.persistence.Transient; import javax.persistence.Transient;
import org.hibernate.annotations.Type;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.Duration; import org.joda.time.Duration;
@ -117,7 +116,6 @@ public abstract class EppResource extends BackupGroupRoot implements Buildable {
DateTime lastEppUpdateTime; DateTime lastEppUpdateTime;
/** Status values associated with this resource. */ /** Status values associated with this resource. */
@Type(type = "google.registry.model.eppcommon.StatusValue$StatusValueSetType")
@Column(name = "statuses") @Column(name = "statuses")
// TODO(mmuller): rename to "statuses" once we're off datastore. // TODO(mmuller): rename to "statuses" once we're off datastore.
Set<StatusValue> status; Set<StatusValue> status;

View file

@ -185,7 +185,6 @@ public class DomainBase extends EppResource
String idnTableName; String idnTableName;
/** Fully qualified host names of this domain's active subordinate hosts. */ /** Fully qualified host names of this domain's active subordinate hosts. */
@org.hibernate.annotations.Type(type = "google.registry.persistence.StringSetUserType")
Set<String> subordinateHosts; Set<String> subordinateHosts;
/** When this domain's registration will expire. */ /** When this domain's registration will expire. */

View file

@ -25,7 +25,6 @@ import google.registry.model.domain.DomainBase;
import google.registry.model.host.HostResource; import google.registry.model.host.HostResource;
import google.registry.model.translators.EnumToAttributeAdapter.EppEnum; import google.registry.model.translators.EnumToAttributeAdapter.EppEnum;
import google.registry.model.translators.StatusValueAdapter; import google.registry.model.translators.StatusValueAdapter;
import google.registry.persistence.EnumSetUserType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/** /**
@ -166,6 +165,4 @@ public enum StatusValue implements EppEnum {
return StatusValue.valueOf(LOWER_CAMEL.to(UPPER_UNDERSCORE, nullToEmpty(xmlName))); return StatusValue.valueOf(LOWER_CAMEL.to(UPPER_UNDERSCORE, nullToEmpty(xmlName)));
} }
/** Hibernate type for sets of {@link StatusValue}. */
public static class StatusValueSetType extends EnumSetUserType<StatusValue> {}
} }

View file

@ -262,15 +262,12 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
State state; State state;
/** The set of TLDs which this registrar is allowed to access. */ /** The set of TLDs which this registrar is allowed to access. */
// TODO(b/147908600): Investigate how to automatically apply user type
@org.hibernate.annotations.Type(type = "google.registry.persistence.StringSetUserType")
Set<String> allowedTlds; Set<String> allowedTlds;
/** Host name of WHOIS server. */ /** Host name of WHOIS server. */
String whoisServer; String whoisServer;
/** Base URLs for the registrar's RDAP servers. */ /** Base URLs for the registrar's RDAP servers. */
@org.hibernate.annotations.Type(type = "google.registry.persistence.StringSetUserType")
Set<String> rdapBaseUrls; Set<String> rdapBaseUrls;
/** /**
@ -298,7 +295,6 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable
String failoverClientCertificateHash; String failoverClientCertificateHash;
/** A whitelist of netmasks (in CIDR notation) which the client is allowed to connect from. */ /** A whitelist of netmasks (in CIDR notation) which the client is allowed to connect from. */
@org.hibernate.annotations.Type(type = "google.registry.persistence.CidrAddressBlockListUserType")
List<CidrAddressBlock> ipAddressWhitelist; List<CidrAddressBlock> ipAddressWhitelist;
/** A hashed password for EPP access. The hash is a base64 encoded SHA256 string. */ /** A hashed password for EPP access. The hash is a base64 encoded SHA256 string. */

View file

@ -42,14 +42,12 @@ import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder; import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable; import google.registry.model.Jsonifiable;
import google.registry.model.annotations.ReportedOn; import google.registry.model.annotations.ReportedOn;
import google.registry.persistence.EnumSetUserType;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Transient; import javax.persistence.Transient;
import org.hibernate.annotations.Type;
/** /**
* A contact for a Registrar. Note, equality, hashCode and comparable have been overridden to only * A contact for a Registrar. Note, equality, hashCode and comparable have been overridden to only
@ -103,9 +101,6 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable {
this.displayName = display; this.displayName = display;
this.required = required; this.required = required;
} }
/** Hibernate type for sets of {@link Type}. */
public static class RegistrarPocType extends EnumSetUserType<Type> {}
} }
/** The name of the contact. */ /** The name of the contact. */
@ -127,8 +122,6 @@ public class RegistrarContact extends ImmutableObject implements Jsonifiable {
* Multiple types are used to associate the registrar contact with various mailing groups. This * Multiple types are used to associate the registrar contact with various mailing groups. This
* data is internal to the registry. * data is internal to the registry.
*/ */
@org.hibernate.annotations.Type(
type = "google.registry.model.registrar.RegistrarContact$Type$RegistrarPocType")
Set<Type> types; Set<Type> types;
/** /**

View file

@ -16,20 +16,24 @@ package google.registry.persistence;
import google.registry.util.CidrAddressBlock; import google.registry.util.CidrAddressBlock;
import java.util.List; import java.util.List;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** /**
* Hibernate {@link org.hibernate.usertype.UserType} for storing/retrieving {@link * JPA {@link AttributeConverter} for storing/retrieving {@link List<CidrAddressBlock>} objects.
* List<CidrAddressBlock>} objects. * TODO(shicong): Investigate if we can have one converter for any List type
*/ */
public class CidrAddressBlockListUserType extends StringListUserType<CidrAddressBlock> { @Converter(autoApply = true)
public class CidrAddressBlockListConverter extends StringListConverterBase<CidrAddressBlock> {
@Override @Override
protected CidrAddressBlock convertToElem(String columnValue) { String toString(CidrAddressBlock element) {
return columnValue == null ? null : CidrAddressBlock.create(columnValue); return element.toString();
} }
@Override @Override
protected String convertToColumn(CidrAddressBlock elementValue) { CidrAddressBlock fromString(String value) {
return elementValue == null ? null : elementValue.toString(); return CidrAddressBlock.create(value);
} }
} }

View file

@ -1,51 +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;
import google.registry.util.TypeUtils.TypeInstantiator;
import java.util.HashSet;
import java.util.Set;
/** Abstract Hibernate user type for storing/retrieving {@link Set<Enum<E>>}. */
public class EnumSetUserType<E extends Enum<E>>
extends GenericCollectionUserType<Set<E>, E, String> {
@Override
Set<E> getNewCollection() {
return new HashSet<>();
}
@Override
ArrayColumnType getColumnType() {
return ArrayColumnType.STRING;
}
@Override
public Class returnedClass() {
return Set.class;
}
@Override
protected E convertToElem(String columnValue) {
return columnValue == null
? null
: Enum.valueOf(new TypeInstantiator<E>(getClass()) {}.getExactType(), columnValue);
}
@Override
protected String convertToColumn(E elementValue) {
return elementValue == null ? null : elementValue.toString();
}
}

View file

@ -1,119 +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;
import google.registry.util.TypeUtils.TypeInstantiator;
import java.sql.Array;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Collection;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
/**
* Generic Hibernate user type to store/retrieve Java collection as an array in Cloud SQL.
*
* @param <T> the concrete {@link Collection} type of the entity field
* @param <E> the Java type of the element for the collection of the entity field
* @param <C> the JDBC supported type of the element in the DB column array
*/
public abstract class GenericCollectionUserType<T extends Collection<E>, E, C>
extends MutableUserType {
abstract T getNewCollection();
abstract ArrayColumnType getColumnType();
enum ArrayColumnType {
STRING(Types.ARRAY, "text");
final int typeCode;
final String typeName;
ArrayColumnType(int typeCode, String typeName) {
this.typeCode = typeCode;
this.typeName = typeName;
}
int getTypeCode() {
return typeCode;
}
String getTypeName() {
return typeName;
}
String getTypeDdlName() {
return typeName + "[]";
}
}
@Override
public Class returnedClass() {
return new TypeInstantiator<T>(getClass()) {}.getExactType();
}
@Override
public int[] sqlTypes() {
return new int[] {getColumnType().getTypeCode()};
}
@Override
public Object nullSafeGet(
ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
if (rs.getArray(names[0]) != null) {
T result = getNewCollection();
for (C element : (C[]) rs.getArray(names[0]).getArray()) {
result.add(convertToElem(element));
}
return result;
}
return null;
}
@Override
public void nullSafeSet(
PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
throws HibernateException, SQLException {
if (value == null) {
st.setArray(index, null);
return;
}
T collection = (T) value;
Array arr =
st.getConnection()
.createArrayOf(
getColumnType().getTypeName(),
collection.stream().map(this::convertToColumn).toArray());
st.setArray(index, arr);
}
/**
* Override this to convert an element value retrieved from the database to a different type.
*
* <p>This method is useful when encoding a java type to one of the types that can be used as an
* array element.
*/
protected E convertToElem(C columnValue) {
return (E) columnValue;
}
protected C convertToColumn(E elementValue) {
return (C) elementValue;
}
}

View file

@ -13,9 +13,10 @@
// limitations under the License. // limitations under the License.
package google.registry.persistence; package google.registry.persistence;
import google.registry.persistence.GenericCollectionUserType.ArrayColumnType;
import java.sql.Types; import java.sql.Types;
import org.hibernate.boot.model.TypeContributions;
import org.hibernate.dialect.PostgreSQL95Dialect; import org.hibernate.dialect.PostgreSQL95Dialect;
import org.hibernate.service.ServiceRegistry;
/** Nomulus mapping rules for column types in Postgresql. */ /** Nomulus mapping rules for column types in Postgresql. */
public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect { public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
@ -25,8 +26,15 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
registerColumnType(Types.TIMESTAMP_WITH_TIMEZONE, "timestamptz"); registerColumnType(Types.TIMESTAMP_WITH_TIMEZONE, "timestamptz");
registerColumnType(Types.TIMESTAMP, "timestamptz"); registerColumnType(Types.TIMESTAMP, "timestamptz");
registerColumnType(Types.OTHER, "hstore"); registerColumnType(Types.OTHER, "hstore");
for (ArrayColumnType arrayType : ArrayColumnType.values()) { registerColumnType(
registerColumnType(arrayType.getTypeCode(), arrayType.getTypeDdlName()); StringCollectionDescriptor.COLUMN_TYPE, StringCollectionDescriptor.COLUMN_DDL_NAME);
} }
@Override
public void contributeTypes(
TypeContributions typeContributions, ServiceRegistry serviceRegistry) {
super.contributeTypes(typeContributions, serviceRegistry);
typeContributions.contributeJavaTypeDescriptor(new StringCollectionDescriptor());
typeContributions.contributeSqlTypeDescriptor(new StringCollectionDescriptor());
} }
} }

View file

@ -0,0 +1,34 @@
// 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;
import google.registry.model.registrar.RegistrarContact.Type;
import java.util.Set;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA {@link AttributeConverter} for storing/retrieving {@link Set<Type>}. */
@Converter(autoApply = true)
public class RegistrarPocSetConverter extends StringSetConverterBase<Type> {
@Override
String toString(Type element) {
return element.name();
}
@Override
Type fromString(String value) {
return Type.valueOf(value);
}
}

View file

@ -0,0 +1,35 @@
// 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;
import google.registry.model.eppcommon.StatusValue;
import java.util.Set;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** JPA {@link AttributeConverter} for storing/retrieving {@link Set<StatusValue>}. */
@Converter(autoApply = true)
public class StatusValueSetConverter extends StringSetConverterBase<StatusValue> {
@Override
String toString(StatusValue element) {
return element.name();
}
@Override
StatusValue fromString(String value) {
return StatusValue.valueOf(value);
}
}

View file

@ -0,0 +1,187 @@
// 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;
import static google.registry.persistence.StringCollectionDescriptor.StringCollection;
import com.google.common.collect.ImmutableList;
import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Collection;
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 StringCollection}.
*
* <p>A {@link StringCollection} object is a simple wrapper for a {@link Collection<String>} which
* can be stored as a string array in the database. The {@link JavaTypeDescriptor} and {@link
* SqlTypeDescriptor} is used by JPA/Hibernate to map between the collection and {@link Array} which
* is the actual type that JDBC uses to read from and write to the database.
*
* @see <a
* href="https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#basic-jpa-convert">JPA
* 2.1 AttributeConverters</a>
*/
public class StringCollectionDescriptor extends AbstractTypeDescriptor<StringCollection>
implements SqlTypeDescriptor {
public static final int COLUMN_TYPE = Types.ARRAY;
public static final String COLUMN_NAME = "text";
public static final String COLUMN_DDL_NAME = COLUMN_NAME + "[]";
protected StringCollectionDescriptor() {
super(StringCollection.class);
}
@Override
public StringCollection fromString(String string) {
throw new UnsupportedOperationException(
"Constructing StringCollectionDescriptor from string is not allowed");
}
@Override
public <X> X unwrap(StringCollection value, Class<X> type, WrapperOptions options) {
if (value == null) {
return null;
}
if (Collection.class.isAssignableFrom(type)) {
return (X) value.getCollection();
}
throw unknownUnwrap(type);
}
@Override
public <X> StringCollection wrap(X value, WrapperOptions options) {
if (value == null) {
return null;
}
if (value instanceof Array) {
try {
String[] arr = (String[]) ((Array) value).getArray();
ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
for (String str : arr) {
builder.add(str);
}
return StringCollection.create(builder.build());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
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 <X> ValueBinder<X> getBinder(JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicBinder<X>(javaTypeDescriptor, this) {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
if (value == null) {
st.setArray(index, null);
return;
}
if (value instanceof StringCollection) {
StringCollection stringCollection = (StringCollection) value;
if (stringCollection.getCollection() == null) {
st.setArray(index, null);
} else {
st.setArray(
index,
st.getConnection()
.createArrayOf(COLUMN_NAME, stringCollection.getCollection().toArray()));
}
} else {
throw new UnsupportedOperationException(
String.format(
"Binding type %s is not supported by StringCollectionDescriptor",
value.getClass().getName()));
}
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
// CallableStatement.setArray() doesn't have an overload version for setting array by its
// column name
throw new UnsupportedOperationException(
"Binding array by its column name is not supported");
}
};
}
@Override
public <X> ValueExtractor<X> getExtractor(JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicExtractor<X>(javaTypeDescriptor, this) {
@Override
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap(rs.getArray(name), options);
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options)
throws SQLException {
return javaTypeDescriptor.wrap(statement.getArray(index), options);
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options)
throws SQLException {
return javaTypeDescriptor.wrap(statement.getArray(name), options);
}
};
}
public static class StringCollection {
private Collection<String> collection;
private StringCollection(Collection<String> collection) {
this.collection = collection;
}
public static StringCollection create(Collection<String> collection) {
return new StringCollection(collection);
}
public Collection<String> getCollection() {
return collection;
}
}
}

View file

@ -14,19 +14,21 @@
package google.registry.persistence; package google.registry.persistence;
import com.google.common.collect.Lists;
import java.util.List; import java.util.List;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** Abstract Hibernate user type for storing/retrieving {@link List<String>}. */ /** JPA {@link AttributeConverter} for storing/retrieving {@link List<String>}. */
public class StringListUserType<E> extends GenericCollectionUserType<List<E>, E, String> { @Converter(autoApply = true)
public class StringListConverter extends StringListConverterBase<String> {
@Override @Override
List<E> getNewCollection() { String toString(String element) {
return Lists.newArrayList(); return element;
} }
@Override @Override
ArrayColumnType getColumnType() { String fromString(String value) {
return ArrayColumnType.STRING; return value;
} }
} }

View file

@ -0,0 +1,48 @@
// 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;
import static com.google.common.collect.ImmutableList.toImmutableList;
import google.registry.persistence.StringCollectionDescriptor.StringCollection;
import java.util.List;
import javax.persistence.AttributeConverter;
/**
* Base JPA converter for {@link List} objects that are stored as an array of strings in the
* database.
*/
public abstract class StringListConverterBase<T>
implements AttributeConverter<List<T>, StringCollection> {
abstract String toString(T element);
abstract T fromString(String value);
@Override
public StringCollection convertToDatabaseColumn(List<T> attribute) {
return attribute == null
? null
: StringCollection.create(
attribute.stream().map(this::toString).collect(toImmutableList()));
}
@Override
public List<T> convertToEntityAttribute(StringCollection dbData) {
return dbData == null || dbData.getCollection() == null
? null
: dbData.getCollection().stream().map(this::fromString).collect(toImmutableList());
}
}

View file

@ -14,19 +14,21 @@
package google.registry.persistence; package google.registry.persistence;
import com.google.common.collect.Sets;
import java.util.Set; import java.util.Set;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
/** Abstract Hibernate user type for storing/retrieving {@link Set<String>}. */ /** JPA {@link AttributeConverter} for storing/retrieving {@link Set<String>}. */
public class StringSetUserType<E> extends GenericCollectionUserType<Set<E>, E, String> { @Converter(autoApply = true)
public class StringSetConverter extends StringSetConverterBase<String> {
@Override @Override
Set<E> getNewCollection() { String toString(String element) {
return Sets.newHashSet(); return element;
} }
@Override @Override
ArrayColumnType getColumnType() { String fromString(String value) {
return ArrayColumnType.STRING; return value;
} }
} }

View file

@ -0,0 +1,47 @@
// 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;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import google.registry.persistence.StringCollectionDescriptor.StringCollection;
import java.util.Set;
import javax.persistence.AttributeConverter;
/**
* Base JPA converter for {@link Set} objects that are stored as an array of strings in the
* database.
*/
public abstract class StringSetConverterBase<T>
implements AttributeConverter<Set<T>, StringCollection> {
abstract String toString(T element);
abstract T fromString(String value);
@Override
public StringCollection convertToDatabaseColumn(Set<T> attribute) {
return attribute == null
? null
: StringCollection.create(attribute.stream().map(this::toString).collect(toImmutableSet()));
}
@Override
public Set<T> convertToEntityAttribute(StringCollection dbData) {
return dbData == null || dbData.getCollection() == null
? null
: dbData.getCollection().stream().map(this::fromString).collect(toImmutableSet());
}
}

View file

@ -39,9 +39,14 @@
<!-- Customized type converters --> <!-- Customized type converters -->
<class>google.registry.persistence.BloomFilterConverter</class> <class>google.registry.persistence.BloomFilterConverter</class>
<class>google.registry.persistence.CidrAddressBlockListConverter</class>
<class>google.registry.persistence.CreateAutoTimestampConverter</class> <class>google.registry.persistence.CreateAutoTimestampConverter</class>
<class>google.registry.persistence.CurrencyUnitConverter</class> <class>google.registry.persistence.CurrencyUnitConverter</class>
<class>google.registry.persistence.DateTimeConverter</class> <class>google.registry.persistence.DateTimeConverter</class>
<class>google.registry.persistence.RegistrarPocSetConverter</class>
<class>google.registry.persistence.StatusValueSetConverter</class>
<class>google.registry.persistence.StringListConverter</class>
<class>google.registry.persistence.StringSetConverter</class>
<class>google.registry.persistence.UpdateAutoTimestampConverter</class> <class>google.registry.persistence.UpdateAutoTimestampConverter</class>
<class>google.registry.persistence.ZonedDateTimeConverter</class> <class>google.registry.persistence.ZonedDateTimeConverter</class>

View file

@ -25,13 +25,12 @@ import google.registry.util.CidrAddressBlock;
import java.util.List; import java.util.List;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import org.hibernate.annotations.Type;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
/** Unit tests for {@link CidrAddressBlockListUserType}. */ /** Unit tests for {@link CidrAddressBlockListConverter}. */
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class CidrAddressBlockListUserTypeTest { public class CidrAddressBlockListUserTypeTest {
@Rule @Rule
@ -59,7 +58,6 @@ public class CidrAddressBlockListUserTypeTest {
@Id String name = "id"; @Id String name = "id";
@Type(type = "google.registry.persistence.CidrAddressBlockListUserType")
List<CidrAddressBlock> addresses; List<CidrAddressBlock> addresses;
private TestEntity() {} private TestEntity() {}

View file

@ -18,29 +18,27 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm; import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import google.registry.model.eppcommon.StatusValue;
import google.registry.persistence.transaction.JpaTestRules; import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule; import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule;
import java.util.Set; import java.util.Set;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import org.hibernate.annotations.Type;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
/** Unit tests for {@link EnumSetUserType}. */ /** Unit tests for {@link StatusValueSetConverter}. */
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class EnumSetUserTypeTest { public class StatusValueSetConverterTest {
@Rule @Rule
public final JpaUnitTestRule jpaRule = public final JpaUnitTestRule jpaRule =
new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule(); new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule();
public EnumSetUserTypeTest() {}
@Test @Test
public void testRoundTrip() { public void testRoundTrip() {
Set<TestEnum> enums = ImmutableSet.of(TestEnum.BAR, TestEnum.FOO); Set<StatusValue> enums = ImmutableSet.of(StatusValue.INACTIVE, StatusValue.PENDING_DELETE);
TestEntity obj = new TestEntity("foo", enums); TestEntity obj = new TestEntity("foo", enums);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(obj)); jpaTm().transact(() -> jpaTm().getEntityManager().persist(obj));
@ -49,45 +47,15 @@ public class EnumSetUserTypeTest {
assertThat(persisted.data).isEqualTo(enums); assertThat(persisted.data).isEqualTo(enums);
} }
@Test
public void testNativeQuery_succeeds() {
Set<TestEnum> enums = ImmutableSet.of(TestEnum.BAR, TestEnum.FOO);
TestEntity obj = new TestEntity("foo", enums);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(obj));
assertThat(
ImmutableSet.of(
getSingleResultFromNativeQuery(
"SELECT data[1] FROM \"TestEntity\" WHERE name = 'foo'"),
getSingleResultFromNativeQuery(
"SELECT data[2] FROM \"TestEntity\" WHERE name = 'foo'")))
.containsExactly("BAR", "FOO");
}
private static Object getSingleResultFromNativeQuery(String sql) {
return jpaTm()
.transact(() -> jpaTm().getEntityManager().createNativeQuery(sql).getSingleResult());
}
enum TestEnum {
FOO,
BAR,
BAZ;
public static class TestEnumType extends EnumSetUserType<TestEnum> {}
}
@Entity(name = "TestEntity") @Entity(name = "TestEntity")
static class TestEntity { static class TestEntity {
@Id String name; @Id String name;
@Type(type = "google.registry.persistence.EnumSetUserTypeTest$TestEnum$TestEnumType") Set<StatusValue> data;
Set<TestEnum> data;
TestEntity() {} TestEntity() {}
TestEntity(String name, Set<TestEnum> data) { TestEntity(String name, Set<StatusValue> data) {
this.name = name; this.name = name;
this.data = data; this.data = data;
} }

View file

@ -26,15 +26,14 @@ import java.util.List;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import org.hibernate.annotations.Type;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
/** Unit tests for {@link StringListUserType}. */ /** Unit tests for {@link StringListConverter}. */
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class StringListUserTypeTest { public class StringListConverterTest {
@Rule @Rule
public final JpaUnitTestRule jpaRule = public final JpaUnitTestRule jpaRule =
new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule(); new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule();
@ -123,7 +122,6 @@ public class StringListUserTypeTest {
@Id String name = "id"; @Id String name = "id";
@Type(type = "google.registry.persistence.StringListUserType")
List<String> tlds; List<String> tlds;
private TestEntity() {} private TestEntity() {}

View file

@ -24,15 +24,14 @@ import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestRule;
import java.util.Set; import java.util.Set;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import org.hibernate.annotations.Type;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
/** Unit tests for {@link StringSetUserType}. */ /** Unit tests for {@link StringSetConverter}. */
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class StringSetUserTypeTest { public class StringSetConverterTest {
@Rule @Rule
public final JpaUnitTestRule jpaRule = public final JpaUnitTestRule jpaRule =
new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule(); new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule();
@ -70,7 +69,6 @@ public class StringSetUserTypeTest {
@Id String name = "id"; @Id String name = "id";
@Type(type = "google.registry.persistence.StringSetUserType")
Set<String> tlds; Set<String> tlds;
private TestEntity() {} private TestEntity() {}