Add serializer for Project

This commit adds a serializer for the Project entitiy. The eventual
goal is that we serialize CachedProjectConfig. The entity is too
large to be serialized directly, though, so we divide an conquer.

Since we use proto3 for serialization, we have to convert null
values to empty strings and back. For all the fields we serialize
we do not actually care about the difference of null and empty
strings.

Change-Id: I1fbba29ced9ad6dd836ef43674ac033960b19edd
diff --git a/java/com/google/gerrit/entities/Project.java b/java/com/google/gerrit/entities/Project.java
index 759d50a..ef3cbeb 100644
--- a/java/com/google/gerrit/entities/Project.java
+++ b/java/com/google/gerrit/entities/Project.java
@@ -103,7 +103,7 @@
   @Nullable
   public abstract String getDescription();
 
-  protected abstract ImmutableMap<BooleanProjectConfig, InheritableBoolean> getBooleanConfigs();
+  public abstract ImmutableMap<BooleanProjectConfig, InheritableBoolean> getBooleanConfigs();
 
   /**
    * Submit type as configured in {@code project.config}.
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/BUILD b/java/com/google/gerrit/server/cache/serialize/entities/BUILD
new file mode 100644
index 0000000..81f8f6b
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/BUILD
@@ -0,0 +1,18 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+    name = "entities",
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/common:annotations",
+        "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/git",
+        "//java/com/google/gerrit/proto",
+        "//lib:guava",
+        "//lib:jgit",
+        "//lib:protobuf",
+        "//proto:cache_java_proto",
+    ],
+)
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/ProjectSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/ProjectSerializer.java
new file mode 100644
index 0000000..aa1f4ce
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/serialize/entities/ProjectSerializer.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2020 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.server.cache.serialize.entities;
+
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.nullToEmpty;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.base.Converter;
+import com.google.common.base.Enums;
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.server.cache.proto.Cache;
+import java.util.Arrays;
+import java.util.Set;
+
+/** Helper to (de)serialize values for caches. */
+public class ProjectSerializer {
+  private static final Converter<String, ProjectState> PROJECT_STATE_CONVERTER =
+      Enums.stringConverter(ProjectState.class);
+  private static final Converter<String, SubmitType> SUBMIT_TYPE_CONVERTER =
+      Enums.stringConverter(SubmitType.class);
+
+  public static Project deserialize(Cache.ProjectProto proto) {
+    Project.Builder builder =
+        Project.builder(Project.nameKey(proto.getName()))
+            .setSubmitType(SUBMIT_TYPE_CONVERTER.convert(proto.getSubmitType()))
+            .setState(PROJECT_STATE_CONVERTER.convert(proto.getState()))
+            .setDescription(emptyToNull(proto.getDescription()))
+            .setParent(emptyToNull(proto.getParent()))
+            .setMaxObjectSizeLimit(emptyToNull(proto.getMaxObjectSizeLimit()))
+            .setDefaultDashboard(emptyToNull(proto.getDefaultDashboard()))
+            .setLocalDefaultDashboard(emptyToNull(proto.getLocalDefaultDashboard()))
+            .setConfigRefState(emptyToNull(proto.getConfigRefState()));
+
+    Set<String> configs =
+        Arrays.stream(BooleanProjectConfig.values())
+            .map(BooleanProjectConfig::name)
+            .collect(toImmutableSet());
+    proto
+        .getBooleanConfigsMap()
+        .entrySet()
+        .forEach(
+            configEntry -> {
+              if (configs.contains(configEntry.getKey())) {
+                builder.setBooleanConfig(
+                    BooleanProjectConfig.valueOf(configEntry.getKey()),
+                    InheritableBoolean.valueOf(configEntry.getValue()));
+              }
+            });
+
+    return builder.build();
+  }
+
+  public static Cache.ProjectProto serialize(Project autoValue) {
+    Cache.ProjectProto.Builder builder =
+        Cache.ProjectProto.newBuilder()
+            .setName(autoValue.getName())
+            .setSubmitType(SUBMIT_TYPE_CONVERTER.reverse().convert(autoValue.getSubmitType()))
+            .setState(PROJECT_STATE_CONVERTER.reverse().convert(autoValue.getState()))
+            .setDescription(nullToEmpty(autoValue.getDescription()))
+            .setParent(nullToEmpty(autoValue.getParentName()))
+            .setMaxObjectSizeLimit(nullToEmpty(autoValue.getMaxObjectSizeLimit()))
+            .setDefaultDashboard(nullToEmpty(autoValue.getDefaultDashboard()))
+            .setLocalDefaultDashboard(nullToEmpty(autoValue.getLocalDefaultDashboard()))
+            .setConfigRefState(nullToEmpty(autoValue.getConfigRefState()));
+
+    autoValue
+        .getBooleanConfigs()
+        .entrySet()
+        .forEach(
+            configEntry -> {
+              builder.putBooleanConfigs(configEntry.getKey().name(), configEntry.getValue().name());
+            });
+
+    return builder.build();
+  }
+
+  private ProjectSerializer() {}
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD b/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD
new file mode 100644
index 0000000..c781d86
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD
@@ -0,0 +1,25 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "tests",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/entities",
+        "//java/com/google/gerrit/extensions:api",
+        "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/server/cache/serialize",
+        "//java/com/google/gerrit/server/cache/serialize/entities",
+        "//java/com/google/gerrit/server/cache/testing",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//lib:guava",
+        "//lib:jgit",
+        "//lib:junit",
+        "//lib:protobuf",
+        "//lib/auto:auto-value",
+        "//lib/auto:auto-value-annotations",
+        "//lib/truth",
+        "//lib/truth:truth-proto-extension",
+        "//proto:cache_java_proto",
+        "//proto/testing:test_java_proto",
+    ],
+)
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/ProjectSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/ProjectSerializerTest.java
new file mode 100644
index 0000000..8d13247
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/ProjectSerializerTest.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2020 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.server.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.ProjectSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.ProjectSerializer.serialize;
+
+import com.google.gerrit.entities.BooleanProjectConfig;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
+import org.junit.Test;
+
+public class ProjectSerializerTest {
+  @Test
+  public void roundTrip() {
+    Project projectAutoValue =
+        Project.builder(Project.nameKey("test"))
+            .setDescription("desc")
+            .setSubmitType(SubmitType.FAST_FORWARD_ONLY)
+            .setState(ProjectState.HIDDEN)
+            .setParent(Project.nameKey("parent"))
+            .setMaxObjectSizeLimit("11K")
+            .setDefaultDashboard("dashboard1")
+            .setLocalDefaultDashboard("dashboard2")
+            .setConfigRefState("1337")
+            .setBooleanConfig(
+                BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.TRUE)
+            .setBooleanConfig(
+                BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+                InheritableBoolean.INHERIT)
+            .build();
+
+    assertThat(deserialize(serialize(projectAutoValue))).isEqualTo(projectAutoValue);
+  }
+
+  @Test
+  public void roundTripWithMinimalValues() {
+    Project projectAutoValue =
+        Project.builder(Project.nameKey("test"))
+            .setSubmitType(SubmitType.FAST_FORWARD_ONLY)
+            .setState(ProjectState.HIDDEN)
+            .build();
+
+    assertThat(deserialize(serialize(projectAutoValue))).isEqualTo(projectAutoValue);
+  }
+}
diff --git a/proto/cache.proto b/proto/cache.proto
index e157608..4d24036 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -323,3 +323,18 @@
   repeated ProjectWatchProto project_watch_proto = 2;
   string user_preferences = 3;
 }
+
+// Serialized form of com.google.gerrit.entities.Project.
+// Next ID: 11
+message ProjectProto {
+  string name = 1;
+  string description = 2;
+  map<string, string> boolean_configs = 3;
+  string submit_type = 4; // ENUM as String
+  string state = 5; // ENUM as String
+  string parent = 6;
+  string max_object_size_limit = 7;
+  string default_dashboard = 8;
+  string local_default_dashboard = 9;
+  string config_ref_state = 10;
+}