blob: 27e4b17e3709797f4bfe8d6feda0cc9cc3e36f93 [file] [log] [blame]
// 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);
}
}