Add code to generate ProtocolBuffers from Gerrit API POJO definitions

This commit adds a first version that I consider work in progress.
We want to generate protocol buffers from the existing API objects.

In an idea future world, we reduce the current duplication of API
data definition we have (Java POJOs, docs, Typescript + 1 for any
programming language any API client uses) to just a single place.

Protocol Buffers seem interesting for that, so we start simply
by offering a tool to generate *.proto files from POJO files. In
fact, we don't even generate files yet, we just dump output to the
CLI.

Known shortcomings and future work:
- Handle list multimaps
- Write files
- Parse doc strings and unify Ascidoc DOCS and POJO docs

Release-Notes: skip
Change-Id: Ie923cf74b6f1916065bb97abde132f9502cc42e8
diff --git a/java/com/google/gerrit/util/cli/ApiProtocolBufferGenerator.java b/java/com/google/gerrit/util/cli/ApiProtocolBufferGenerator.java
new file mode 100644
index 0000000..27e4b17
--- /dev/null
+++ b/java/com/google/gerrit/util/cli/ApiProtocolBufferGenerator.java
@@ -0,0 +1,176 @@
+// Copyright (C) 2023 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.util.cli;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.reflect.ClassPath;
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Utility to generate Protocol Buffers (*.proto) files from existing POJO API types.
+ *
+ * <p>Usage:
+ *
+ * <ul>
+ *   <li>Print proto representation of all API objects: {@code bazelisk run
+ *       java/com/google/gerrit/util/cli:protogen}
+ * </ul>
+ */
+public class ApiProtocolBufferGenerator {
+  private static String NOTICE =
+      "// Copyright (C) 2023 The Android Open Source Project\n"
+          + "//\n"
+          + "// Licensed under the Apache License, Version 2.0 (the \"License\");\n"
+          + "// you may not use this file except in compliance with the License.\n"
+          + "// You may obtain a copy of the License at\n"
+          + "//\n"
+          + "// http://www.apache.org/licenses/LICENSE-2.0\n"
+          + "//\n"
+          + "// Unless required by applicable law or agreed to in writing, software\n"
+          + "// distributed under the License is distributed on an \"AS IS\" BASIS,\n"
+          + "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"
+          + "// See the License for the specific language governing permissions and\n"
+          + "// limitations under the License.";
+
+  private static String PACKAGE = "com.google.gerrit.extensions.common";
+
+  public static void main(String[] args) {
+    try {
+      ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses().stream()
+          .filter(c -> c.getPackageName().equalsIgnoreCase(PACKAGE))
+          .filter(c -> c.getName().endsWith("Input") || c.getName().endsWith("Info"))
+          .map(clazz -> clazz.load())
+          .forEach(ApiProtocolBufferGenerator::exportSingleClass);
+    } catch (Exception e) {
+      System.err.println(e);
+    }
+  }
+
+  private static void exportSingleClass(Class<?> clazz) {
+    StringBuilder proto = new StringBuilder(NOTICE);
+    proto.append("\n\nsyntax = \"proto3\";");
+    proto.append("\n\npackage gerrit.api;");
+    proto.append("\n\noption java_package = \"" + PACKAGE + "\";");
+
+    int fieldNumber = 1;
+
+    proto.append("\n\n\nmessage " + clazz.getSimpleName() + " {\n");
+
+    for (Field f : clazz.getFields()) {
+      Class<?> type = f.getType();
+
+      if (type.isAssignableFrom(List.class)) {
+        ParameterizedType list = (ParameterizedType) f.getGenericType();
+        Class<?> genericType = (Class<?>) list.getActualTypeArguments()[0];
+        String protoType =
+            protoType(genericType)
+                .orElseThrow(() -> new IllegalStateException("unknown type: " + genericType));
+        proto.append(
+            String.format(
+                "repeated %s %s = %d;\n", protoType, protoName(f.getName()), fieldNumber));
+      } else if (type.isAssignableFrom(Map.class)) {
+        ParameterizedType map = (ParameterizedType) f.getGenericType();
+        Class<?> key = (Class<?>) map.getActualTypeArguments()[0];
+        if (map.getActualTypeArguments()[1] instanceof ParameterizedType) {
+          // TODO: This is list multimap which proto doesn't support. Move to
+          // it's own types.
+          proto.append(
+              "reserved "
+                  + fieldNumber
+                  + "; // TODO(hiesel): Add support for map<?,repeated <?>>\n");
+        } else {
+          Class<?> value = (Class<?>) map.getActualTypeArguments()[1];
+          String keyProtoType =
+              protoType(key).orElseThrow(() -> new IllegalStateException("unknown type: " + key));
+          String valueProtoType =
+              protoType(value)
+                  .orElseThrow(() -> new IllegalStateException("unknown type: " + value));
+          proto.append(
+              String.format(
+                  "map<%s,%s> %s = %d;\n",
+                  keyProtoType, valueProtoType, protoName(f.getName()), fieldNumber));
+        }
+      } else if (protoType(type).isPresent()) {
+        proto.append(
+            String.format(
+                "%s %s = %d;\n", protoType(type).get(), protoName(f.getName()), fieldNumber));
+      } else {
+        proto.append(
+            "reserved "
+                + fieldNumber
+                + "; // TODO(hiesel): Add support for "
+                + type.getName()
+                + "\n");
+      }
+      fieldNumber++;
+    }
+    proto.append("}");
+
+    System.out.println(proto);
+  }
+
+  private static Optional<String> protoType(Class<?> type) {
+    if (isInt(type)) {
+      return Optional.of("int32");
+    } else if (isLong(type)) {
+      return Optional.of("int64");
+    } else if (isChar(type)) {
+      return Optional.of("string");
+    } else if (isShort(type)) {
+      return Optional.of("int32");
+    } else if (isShort(type)) {
+      return Optional.of("int32");
+    } else if (isBoolean(type)) {
+      return Optional.of("bool");
+    } else if (type.isAssignableFrom(String.class)) {
+      return Optional.of("string");
+    } else if (type.isAssignableFrom(Timestamp.class)) {
+      // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#timestamp
+      return Optional.of("string");
+    } else if (type.getPackageName().startsWith("com.google.gerrit.extensions")) {
+      return Optional.of("gerrit.api." + type.getSimpleName());
+    }
+    return Optional.empty();
+  }
+
+  private static boolean isInt(Class<?> type) {
+    return type.isAssignableFrom(Integer.class) || type.isAssignableFrom(int.class);
+  }
+
+  private static boolean isLong(Class<?> type) {
+    return type.isAssignableFrom(Long.class) || type.isAssignableFrom(long.class);
+  }
+
+  private static boolean isChar(Class<?> type) {
+    return type.isAssignableFrom(Character.class) || type.isAssignableFrom(char.class);
+  }
+
+  private static boolean isShort(Class<?> type) {
+    return type.isAssignableFrom(Short.class) || type.isAssignableFrom(short.class);
+  }
+
+  private static boolean isBoolean(Class<?> type) {
+    return type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class);
+  }
+
+  private static String protoName(String name) {
+    return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, name);
+  }
+}
diff --git a/java/com/google/gerrit/util/cli/BUILD b/java/com/google/gerrit/util/cli/BUILD
index ebcc67e..b464f32 100644
--- a/java/com/google/gerrit/util/cli/BUILD
+++ b/java/com/google/gerrit/util/cli/BUILD
@@ -2,7 +2,10 @@
 
 java_library(
     name = "cli",
-    srcs = glob(["**/*.java"]),
+    srcs = glob(
+        ["**/*.java"],
+        exclude = ["ApiProtocolBufferGenerator.java"],
+    ),
     visibility = ["//visibility:public"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
@@ -14,3 +17,15 @@
         "//lib/guice:guice-assistedinject",
     ],
 )
+
+# Util to generate *.proto files from *Info and *Input objects
+java_binary(
+    name = "protogen",
+    srcs = ["ApiProtocolBufferGenerator.java"],
+    main_class = "com.google.gerrit.util.cli.ApiProtocolBufferGenerator",
+    deps = [
+        "//java/com/google/gerrit/extensions:api",
+        "//lib:guava",
+        "//lib:protobuf",
+    ],
+)