Convert Project#NameKey to an interface and its implementors to records

This was previously implemented as an immutable abstract class. However,
for Goggle-internal reasons, we need to convert the subclasses to
records. As records do not support inheritance, the Project#NameKey
parent class has become an interface.

Release-Notes: skip
Google-Bug: b/442161498
Change-Id: I8de8470cf9a20bcd1814236295c937890d346535
Forward-Compatible: checked'
diff --git a/java/com/google/gerrit/entities/GeneralProjectName.java b/java/com/google/gerrit/entities/GeneralProjectName.java
new file mode 100644
index 0000000..92bd178
--- /dev/null
+++ b/java/com/google/gerrit/entities/GeneralProjectName.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// 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 com.google.gerrit.entities;
+
+import com.google.gerrit.common.ConvertibleToProto;
+import com.google.gerrit.entities.Project.NameKey;
+
+@ConvertibleToProto
+public record GeneralProjectName(String name) implements Project.NameKey {
+
+  private static final long serialVersionUID = 1L;
+
+  public GeneralProjectName(NameKey nameKey) {
+    this(nameKey.name());
+  }
+
+  @Override
+  public int hashCode() {
+    return projectNameHashCode();
+  }
+
+  @Override
+  public boolean equals(Object b) {
+    return projectNameEquals(b);
+  }
+
+  @Override
+  public String toString() {
+    return projectNameToString();
+  }
+}
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index 7b02597..45c3bc0 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.entities;
 
-import static java.util.Objects.requireNonNull;
-
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableMap;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -41,63 +39,64 @@
   public static final SubmitType DEFAULT_ALL_PROJECTS_SUBMIT_TYPE = SubmitType.MERGE_IF_NECESSARY;
 
   public static NameKey nameKey(String name) {
-    return new NameKey(name);
+    return new GeneralProjectName(name);
   }
 
   /**
    * Project name key.
    *
-   * <p>This class has subclasses such as {@code AllProjectsName}, which make Guice injection more
-   * convenient. Subclasses must compare equal if they have the same name, regardless of the
-   * specific class. This implies that subclasses may not add additional fields.
+   * <p>This interface has a few implementations. Callers should normally only interact with the
+   * interface directly, and create an instance using {@link #nameKey}.
    *
-   * <p>Because of this unusual subclassing behavior, this class is not an {@code @AutoValue},
-   * unlike other key types in this package. However, this is strictly an implementation detail; its
-   * interface and semantics are otherwise analogous to the {@code @AutoValue} types.
+   * <p>The main implementation is {@code GeneralProjectName}, which supports any project name.
    *
-   * <p>This class is immutable and thread safe.
+   * <p>Other implementations are specific for reserved repo names, such as {@code AllProjectsName}.
+   * Having them as separate types makes the Guice injection more convenient. Implementors must
+   * compare equal if they have the same name, regardless of the specific class. This implies that
+   * implementors may not add additional fields besides `name`, and that they must override {@code
+   * equals} and {@code hashCode} to use the {@link #projectNameEquals} and {@link
+   * #projectNameHashCode} methods provided by this interface. All implementors must be immutable
+   * and ThreadSafe, please use {@code record} for new implementors.
+   *
+   * <p>Why was it implemented this way? We needed the implementations to be distinguished record
+   * types. As records do not support inheritance, the cleanest way to share the common behavior
+   * between them was this interface. Interfaces cannot override {@link Object}methods, and
+   * therefore the implementors must wrap these on their own.
    */
   @Immutable
   @ConvertibleToProto
-  public static class NameKey implements Serializable, Comparable<NameKey> {
-    private static final long serialVersionUID = 1L;
+  public interface NameKey extends Serializable, Comparable<NameKey> {
+    long serialVersionUID = 1L;
 
     /** Parse a Project.NameKey out of a string representation. */
-    public static NameKey parse(String str) {
+    static NameKey parse(String str) {
       return nameKey(ProjectUtil.sanitizeProjectName(KeyUtil.decode(str)));
     }
 
-    private final String name;
+    String name();
 
-    protected NameKey(String name) {
-      this.name = requireNonNull(name);
+    default String get() {
+      return name();
     }
 
-    public String get() {
-      return name;
+    default int projectNameHashCode() {
+      return name().hashCode();
     }
 
-    @Override
-    public final int hashCode() {
-      return name.hashCode();
-    }
-
-    @Override
-    public final boolean equals(Object b) {
+    default boolean projectNameEquals(Object b) {
       if (b instanceof NameKey) {
-        return name.equals(((NameKey) b).get());
+        return name().equals(((NameKey) b).get());
       }
       return false;
     }
 
-    @Override
-    public final int compareTo(NameKey o) {
-      return name.compareTo(o.get());
+    default String projectNameToString() {
+      return KeyUtil.encode(name());
     }
 
     @Override
-    public final String toString() {
-      return KeyUtil.encode(name);
+    default int compareTo(NameKey o) {
+      return name().compareTo(o.get());
     }
   }
 
diff --git a/java/com/google/gerrit/entities/converter/GeneralProjectNameConverter.java b/java/com/google/gerrit/entities/converter/GeneralProjectNameConverter.java
new file mode 100644
index 0000000..0dd49f6
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/GeneralProjectNameConverter.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// 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 com.google.gerrit.entities.converter;
+
+import com.google.gerrit.entities.GeneralProjectName;
+import com.google.gerrit.proto.Entities;
+import com.google.gerrit.proto.Entities.Project_NameKey;
+import com.google.protobuf.Parser;
+
+public enum GeneralProjectNameConverter
+    implements SafeProtoConverter<Entities.Project_NameKey, GeneralProjectName> {
+  INSTANCE;
+
+  @Override
+  public Project_NameKey toProto(GeneralProjectName nameKey) {
+    return Entities.Project_NameKey.newBuilder().setName(nameKey.get()).build();
+  }
+
+  @Override
+  public GeneralProjectName fromProto(Project_NameKey proto) {
+    return new GeneralProjectName(proto.getName());
+  }
+
+  @Override
+  public Class<Project_NameKey> getProtoClass() {
+    return Project_NameKey.class;
+  }
+
+  @Override
+  public Class<GeneralProjectName> getEntityClass() {
+    return GeneralProjectName.class;
+  }
+
+  @Override
+  public Parser<Project_NameKey> getParser() {
+    return Project_NameKey.parser();
+  }
+}
diff --git a/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java b/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
index 320b8fc..47d624a 100644
--- a/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverter.java
@@ -15,39 +15,28 @@
 package com.google.gerrit.entities.converter;
 
 import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.GeneralProjectName;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.proto.Entities;
-import com.google.gerrit.proto.Entities.Project_NameKey;
 import com.google.protobuf.Parser;
 
 @Immutable
 public enum ProjectNameKeyProtoConverter
-    implements SafeProtoConverter<Entities.Project_NameKey, Project.NameKey> {
+    implements ProtoConverter<Entities.Project_NameKey, Project.NameKey> {
   INSTANCE;
 
   @Override
   public Entities.Project_NameKey toProto(Project.NameKey nameKey) {
-    return Entities.Project_NameKey.newBuilder().setName(nameKey.get()).build();
+    return GeneralProjectNameConverter.INSTANCE.toProto(new GeneralProjectName(nameKey));
   }
 
   @Override
-  public Project.NameKey fromProto(Entities.Project_NameKey proto) {
-    return Project.nameKey(proto.getName());
+  public GeneralProjectName fromProto(Entities.Project_NameKey proto) {
+    return GeneralProjectNameConverter.INSTANCE.fromProto(proto);
   }
 
   @Override
   public Parser<Entities.Project_NameKey> getParser() {
     return Entities.Project_NameKey.parser();
   }
-
-  @Override
-  public Class<Project_NameKey> getProtoClass() {
-    return Project_NameKey.class;
-  }
-
-  @Override
-  public Class<NameKey> getEntityClass() {
-    return NameKey.class;
-  }
 }
diff --git a/java/com/google/gerrit/server/config/AllProjectsName.java b/java/com/google/gerrit/server/config/AllProjectsName.java
index 3a13a58..cc9bb7a 100644
--- a/java/com/google/gerrit/server/config/AllProjectsName.java
+++ b/java/com/google/gerrit/server/config/AllProjectsName.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.errorprone.annotations.Immutable;
-import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
 
 /**
  * Special name of the project that all projects derive from.
@@ -23,10 +23,21 @@
  * <p>This class is immutable and thread safe.
  */
 @Immutable
-public class AllProjectsName extends Project.NameKey {
+public record AllProjectsName(String name) implements NameKey {
   private static final long serialVersionUID = 1L;
 
-  public AllProjectsName(String name) {
-    super(name);
+  @Override
+  public int hashCode() {
+    return projectNameHashCode();
+  }
+
+  @Override
+  public boolean equals(Object b) {
+    return projectNameEquals(b);
+  }
+
+  @Override
+  public String toString() {
+    return projectNameToString();
   }
 }
diff --git a/java/com/google/gerrit/server/config/AllUsersName.java b/java/com/google/gerrit/server/config/AllUsersName.java
index 393fb6b..6a6df60 100644
--- a/java/com/google/gerrit/server/config/AllUsersName.java
+++ b/java/com/google/gerrit/server/config/AllUsersName.java
@@ -23,10 +23,21 @@
  * <p>This class is immutable and thread safe.
  */
 @Immutable
-public class AllUsersName extends Project.NameKey {
+public record AllUsersName(String name) implements Project.NameKey {
   private static final long serialVersionUID = 1L;
 
-  public AllUsersName(String name) {
-    super(name);
+  @Override
+  public int hashCode() {
+    return projectNameHashCode();
+  }
+
+  @Override
+  public boolean equals(Object b) {
+    return projectNameEquals(b);
+  }
+
+  @Override
+  public String toString() {
+    return projectNameToString();
   }
 }
diff --git a/javatests/com/google/gerrit/entities/BranchTest.java b/javatests/com/google/gerrit/entities/BranchTest.java
index 0483ebc..d75418a 100644
--- a/javatests/com/google/gerrit/entities/BranchTest.java
+++ b/javatests/com/google/gerrit/entities/BranchTest.java
@@ -21,19 +21,19 @@
 public class BranchTest {
   @Test
   public void canonicalizeNameDuringConstruction() {
-    assertThat(BranchNameKey.create(new Project.NameKey("foo"), "bar").branch())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo"), "bar").branch())
         .isEqualTo("refs/heads/bar");
-    assertThat(BranchNameKey.create(new Project.NameKey("foo"), "refs/heads/bar").branch())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo"), "refs/heads/bar").branch())
         .isEqualTo("refs/heads/bar");
   }
 
   @Test
   public void idToString() {
-    assertThat(BranchNameKey.create(new Project.NameKey("foo"), "bar").toString())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo"), "bar").toString())
         .isEqualTo("foo,refs/heads/bar");
-    assertThat(BranchNameKey.create(new Project.NameKey("foo bar"), "bar baz").toString())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo bar"), "bar baz").toString())
         .isEqualTo("foo+bar,refs/heads/bar+baz");
-    assertThat(BranchNameKey.create(new Project.NameKey("foo^bar"), "bar^baz").toString())
+    assertThat(BranchNameKey.create(Project.NameKey.parse("foo^bar"), "bar^baz").toString())
         .isEqualTo("foo%5Ebar,refs/heads/bar%5Ebaz");
   }
 }
diff --git a/javatests/com/google/gerrit/entities/ProjectTest.java b/javatests/com/google/gerrit/entities/ProjectTest.java
index 94a6106..b3b3e0a 100644
--- a/javatests/com/google/gerrit/entities/ProjectTest.java
+++ b/javatests/com/google/gerrit/entities/ProjectTest.java
@@ -21,13 +21,12 @@
 public class ProjectTest {
   @Test
   public void parseId() {
-    assertThat(Project.NameKey.parse("foo")).isEqualTo(new Project.NameKey("foo"));
-    assertThat(Project.NameKey.parse("foo%20bar")).isEqualTo(new Project.NameKey("foo bar"));
-    assertThat(Project.NameKey.parse("foo+bar")).isEqualTo(new Project.NameKey("foo bar"));
-    assertThat(Project.NameKey.parse("foo%2fbar")).isEqualTo(new Project.NameKey("foo/bar"));
-    assertThat(Project.NameKey.parse("foo%2Fbar")).isEqualTo(new Project.NameKey("foo/bar"));
-    assertThat(Project.NameKey.parse("foo/a_bar%2B%2B"))
-        .isEqualTo(new Project.NameKey("foo/a_bar++"));
+    assertThat(Project.NameKey.parse("foo")).isEqualTo(Project.nameKey("foo"));
+    assertThat(Project.NameKey.parse("foo%20bar")).isEqualTo(Project.nameKey("foo bar"));
+    assertThat(Project.NameKey.parse("foo+bar")).isEqualTo(Project.nameKey("foo bar"));
+    assertThat(Project.NameKey.parse("foo%2fbar")).isEqualTo(Project.nameKey("foo/bar"));
+    assertThat(Project.NameKey.parse("foo%2Fbar")).isEqualTo(Project.nameKey("foo/bar"));
+    assertThat(Project.NameKey.parse("foo/a_bar%2B%2B")).isEqualTo(Project.nameKey("foo/a_bar++"));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java
index 2fa89a5..b8b2115 100644
--- a/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ProjectNameKeyProtoConverterTest.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.GeneralProjectName;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
@@ -47,12 +48,14 @@
         projectNameKeyProtoConverter.fromProto(projectNameKeyProtoConverter.toProto(nameKey));
 
     assertThat(convertedNameKey).isEqualTo(nameKey);
+    assertThat(convertedNameKey.getClass()).isEqualTo(GeneralProjectName.class);
   }
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void fieldsExistAsExpected() {
-    assertThatSerializedClass(Project.NameKey.class)
+    assertThat(GeneralProjectName.class.isRecord()).isTrue();
+    assertThatSerializedClass(GeneralProjectName.class)
         .hasFields(ImmutableMap.of("name", String.class));
   }
 }