1
0
mirror of https://github.com/google/nomulus synced 2025-12-23 06:15: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:
Ben McIlwain
2025-10-28 13:10:27 -04:00
committed by GitHub
parent c33f0dc07f
commit 8f69b48e87
7 changed files with 136 additions and 5 deletions

View 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();
}

View File

@@ -76,11 +76,20 @@ public class ModelUtils {
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) {
try {
return field.get(instance);
} catch (IllegalAccessException e) {
if (field.isAnnotationPresent(GetterDelegate.class)) {
return instance
.getClass()
.getMethod(field.getAnnotation(GetterDelegate.class).methodName())
.invoke(instance);
} else {
return field.get(instance);
}
} catch (Exception e) {
throw new IllegalStateException(e);
}
}

View File

@@ -54,6 +54,7 @@ import com.google.gson.annotations.Expose;
import com.google.re2j.Pattern;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.GetterDelegate;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UpdateAutoTimestamp;
@@ -245,12 +246,15 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
State state;
/** 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. */
@Expose String whoisServer;
/** Base URLs for the registrar's RDAP servers. */
@GetterDelegate(methodName = "getRdapBaseUrls")
Set<String> rdapBaseUrls;
/**

View File

@@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gson.annotations.Expose;
import google.registry.model.Buildable;
import google.registry.model.GetterDelegate;
import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
@@ -110,6 +111,7 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
* data is internal to the registry.
*/
@Enumerated(EnumType.STRING)
@GetterDelegate(methodName = "getTypes")
@Expose
Set<Type> types;

View File

@@ -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.DatabaseHelper.cloneAndSetAutoTimestamps;
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.persistResource;
import static google.registry.testing.DatabaseHelper.persistResources;
@@ -760,4 +761,16 @@ class RegistrarTest extends EntityTestCase {
.setAllowedTlds(ImmutableSet.of("tld", "xn--q9jyb4c"))
.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]");
}
}

View File

@@ -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()));
}
}
}
}
}

View File

@@ -203,7 +203,9 @@ class ConsoleUpdateRegistrarActionTest extends ConsoleActionBaseTestCase {
"The following changes were made in registry unittest environment to the"
+ " registrar TheRegistrar by admin fte@email.tld:\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 ->"
+ " 2023-12-12T00:00:00.000Z\n")
.setRecipients(ImmutableList.of(new InternetAddress("notification@test.example")))