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:
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);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
+ " 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")))
|
||||
|
||||
Reference in New Issue
Block a user