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