Merge "Add code to generate ProtocolBuffers from Gerrit API POJO definitions"
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",
+ ],
+)