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