mirror of
https://github.com/google/nomulus
synced 2026-01-03 03:35:42 +00:00
Add a @GetterDelegate annotation for better handling of ImmutableObject fields (#2860)
This allows us to specify a getter delegation to bypass Hibernate's limitations on field types for the purposes of, e.g., using a sorted set in toString() output rather than the base Hibernate unsorted HashSet type. BUG=http://b/448631639
This commit is contained in:
49
core/src/main/java/google/registry/model/GetterDelegate.java
Normal file
49
core/src/main/java/google/registry/model/GetterDelegate.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2025 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.model;
|
||||||
|
|
||||||
|
import static java.lang.annotation.ElementType.FIELD;
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A delegate getter method to be used when getting the value of an {@link ImmutableObject} field.
|
||||||
|
*
|
||||||
|
* <p>This is useful because Hibernate has limitations on what kinds of types can be used to
|
||||||
|
* represent a field value, the most relevant being that it must be mutable. Since we use Guava's
|
||||||
|
* ImmutableCollections widely, this means that a frequent pattern is to e.g. have a field be
|
||||||
|
* declared as a Set (with a HashSet implementation), but then implement a getter method for that
|
||||||
|
* field that returns the desired ImmutableSet or ImmutableSortedSet. For purposes where it matters
|
||||||
|
* that the field be represented using the appropriate type, such as for outputting in sorted order
|
||||||
|
* via toString, then declare a getter delegate as follows:
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* @GetterDelegate(methodName = "getAllowedTlds")
|
||||||
|
* Set<String> allowedTlds;
|
||||||
|
*
|
||||||
|
* public ImmutableSortedSet<String> getAllowedTlds() {
|
||||||
|
* return nullToEmptyImmutableSortedCopy(allowedTlds);
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@Target(FIELD)
|
||||||
|
public @interface GetterDelegate {
|
||||||
|
String methodName();
|
||||||
|
}
|
||||||
@@ -76,11 +76,20 @@ public class ModelUtils {
|
|||||||
return ALL_FIELDS_CACHE.get(clazz);
|
return ALL_FIELDS_CACHE.get(clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieves a field value via reflection. */
|
/**
|
||||||
|
* Retrieves a field value via reflection, using the field's {@link GetterDelegate} if present.
|
||||||
|
*/
|
||||||
static Object getFieldValue(Object instance, Field field) {
|
static Object getFieldValue(Object instance, Field field) {
|
||||||
try {
|
try {
|
||||||
return field.get(instance);
|
if (field.isAnnotationPresent(GetterDelegate.class)) {
|
||||||
} catch (IllegalAccessException e) {
|
return instance
|
||||||
|
.getClass()
|
||||||
|
.getMethod(field.getAnnotation(GetterDelegate.class).methodName())
|
||||||
|
.invoke(instance);
|
||||||
|
} else {
|
||||||
|
return field.get(instance);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import com.google.gson.annotations.Expose;
|
|||||||
import com.google.re2j.Pattern;
|
import com.google.re2j.Pattern;
|
||||||
import google.registry.model.Buildable;
|
import google.registry.model.Buildable;
|
||||||
import google.registry.model.CreateAutoTimestamp;
|
import google.registry.model.CreateAutoTimestamp;
|
||||||
|
import google.registry.model.GetterDelegate;
|
||||||
import google.registry.model.JsonMapBuilder;
|
import google.registry.model.JsonMapBuilder;
|
||||||
import google.registry.model.Jsonifiable;
|
import google.registry.model.Jsonifiable;
|
||||||
import google.registry.model.UpdateAutoTimestamp;
|
import google.registry.model.UpdateAutoTimestamp;
|
||||||
@@ -245,12 +246,15 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
|||||||
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. */
|
||||||
@Expose Set<String> allowedTlds;
|
@GetterDelegate(methodName = "getAllowedTlds")
|
||||||
|
@Expose
|
||||||
|
Set<String> allowedTlds;
|
||||||
|
|
||||||
/** Host name of WHOIS server. */
|
/** Host name of WHOIS server. */
|
||||||
@Expose String whoisServer;
|
@Expose String whoisServer;
|
||||||
|
|
||||||
/** Base URLs for the registrar's RDAP servers. */
|
/** Base URLs for the registrar's RDAP servers. */
|
||||||
|
@GetterDelegate(methodName = "getRdapBaseUrls")
|
||||||
Set<String> rdapBaseUrls;
|
Set<String> rdapBaseUrls;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableSet;
|
|||||||
import com.google.common.collect.ImmutableSortedSet;
|
import com.google.common.collect.ImmutableSortedSet;
|
||||||
import com.google.gson.annotations.Expose;
|
import com.google.gson.annotations.Expose;
|
||||||
import google.registry.model.Buildable;
|
import google.registry.model.Buildable;
|
||||||
|
import google.registry.model.GetterDelegate;
|
||||||
import google.registry.model.ImmutableObject;
|
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;
|
||||||
@@ -110,6 +111,7 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
|
|||||||
* data is internal to the registry.
|
* data is internal to the registry.
|
||||||
*/
|
*/
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@GetterDelegate(methodName = "getTypes")
|
||||||
@Expose
|
@Expose
|
||||||
Set<Type> types;
|
Set<Type> types;
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import static google.registry.testing.CertificateSamples.SAMPLE_CERT2_HASH;
|
|||||||
import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
|
import static google.registry.testing.CertificateSamples.SAMPLE_CERT_HASH;
|
||||||
import static google.registry.testing.DatabaseHelper.cloneAndSetAutoTimestamps;
|
import static google.registry.testing.DatabaseHelper.cloneAndSetAutoTimestamps;
|
||||||
import static google.registry.testing.DatabaseHelper.createTld;
|
import static google.registry.testing.DatabaseHelper.createTld;
|
||||||
|
import static google.registry.testing.DatabaseHelper.createTlds;
|
||||||
import static google.registry.testing.DatabaseHelper.newTld;
|
import static google.registry.testing.DatabaseHelper.newTld;
|
||||||
import static google.registry.testing.DatabaseHelper.persistResource;
|
import static google.registry.testing.DatabaseHelper.persistResource;
|
||||||
import static google.registry.testing.DatabaseHelper.persistResources;
|
import static google.registry.testing.DatabaseHelper.persistResources;
|
||||||
@@ -760,4 +761,16 @@ class RegistrarTest extends EntityTestCase {
|
|||||||
.setAllowedTlds(ImmutableSet.of("tld", "xn--q9jyb4c"))
|
.setAllowedTlds(ImmutableSet.of("tld", "xn--q9jyb4c"))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testToString_sortsAllowedTlds() {
|
||||||
|
createTlds("foo", "bar", "baz", "gon", "tri");
|
||||||
|
persistResource(
|
||||||
|
registrar
|
||||||
|
.asBuilder()
|
||||||
|
.setAllowedTlds(ImmutableSet.of("gon", "bar", "foo", "tri", "baz"))
|
||||||
|
.build());
|
||||||
|
assertThat(Registrar.loadByRegistrarId("registrar").toString())
|
||||||
|
.contains("allowedTlds=[bar, baz, foo, gon, tri]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2025 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import google.registry.model.GetterDelegate;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Unit tests for Hibernate entities. */
|
||||||
|
public class EntitiesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getterDelegates_allMethodNamesExist() throws Exception {
|
||||||
|
ImmutableSet<Class<?>> entityClasses =
|
||||||
|
PersistenceXmlUtility.getManagedClasses().stream()
|
||||||
|
.filter(clazz -> clazz.isAnnotationPresent(Entity.class))
|
||||||
|
.collect(toImmutableSet());
|
||||||
|
for (Class<?> clazz : entityClasses) {
|
||||||
|
for (Field field : clazz.getDeclaredFields()) {
|
||||||
|
if (field.isAnnotationPresent(GetterDelegate.class)) {
|
||||||
|
String methodName = field.getAnnotation(GetterDelegate.class).methodName();
|
||||||
|
// Note that calling getDeclaredMethod(methodName) specifically looks for a method with
|
||||||
|
// no parameters; if there were a method that took e.g. one parameter, you'd need
|
||||||
|
// getDeclaredMethod(methodName, param1) to find it.
|
||||||
|
assertDoesNotThrow(
|
||||||
|
() -> clazz.getDeclaredMethod(methodName),
|
||||||
|
String.format(
|
||||||
|
"Method %s() specified in the GetterDelegate "
|
||||||
|
+ "annotation of field %s could not be found on class %s",
|
||||||
|
methodName, field.getName(), clazz.getCanonicalName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -203,7 +203,9 @@ class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase {
|
|||||||
"The following changes were made in registry unittest environment to the"
|
"The following changes were made in registry unittest environment to the"
|
||||||
+ " registrar TheRegistrar by admin fte@email.tld:\n"
|
+ " registrar TheRegistrar by admin fte@email.tld:\n"
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ "allowedTlds: null -> [app, dev]\n"
|
+ "allowedTlds:\n"
|
||||||
|
+ " ADDED: [app, dev]\n"
|
||||||
|
+ " FINAL CONTENTS: [app, dev]\n"
|
||||||
+ "lastPocVerificationDate: 1970-01-01T00:00:00.000Z ->"
|
+ "lastPocVerificationDate: 1970-01-01T00:00:00.000Z ->"
|
||||||
+ " 2023-12-12T00:00:00.000Z\n")
|
+ " 2023-12-12T00:00:00.000Z\n")
|
||||||
.setRecipients(ImmutableList.of(new InternetAddress("notification@test.example")))
|
.setRecipients(ImmutableList.of(new InternetAddress("notification@test.example")))
|
||||||
|
|||||||
Reference in New Issue
Block a user