blob: 8df320d26d2ba227bcf1a353716b3b6edb7c8fa0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.servicecomb.codec.protobuf.internal.converter;
import static org.apache.servicecomb.foundation.common.utils.StringBuilderUtils.appendLine;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang3.StringUtils;
import org.apache.servicecomb.foundation.protobuf.internal.ProtoConst;
import org.apache.servicecomb.foundation.protobuf.internal.parser.ProtoParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.hash.Hashing;
import io.protostuff.compiler.model.Message;
import io.protostuff.compiler.model.Proto;
import io.swagger.models.Model;
import io.swagger.models.ModelImpl;
import io.swagger.models.Operation;
import io.swagger.models.Path;
import io.swagger.models.Response;
import io.swagger.models.Swagger;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.properties.Property;
import io.vertx.core.json.Json;
public class SwaggerToProtoGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(SwaggerToProtoGenerator.class);
private final String protoPackage;
private final Swagger swagger;
private final StringBuilder msgStringBuilder = new StringBuilder();
private final StringBuilder serviceBuilder = new StringBuilder();
private final Set<String> imports = new HashSet<>();
private final Set<String> messages = new HashSet<>();
private List<Runnable> pending = new ArrayList<>();
// not java package
// better to be: app_${app}.mid_{microservice}.sid_{schemaId}
public SwaggerToProtoGenerator(String protoPackage, Swagger swagger) {
this.protoPackage = escapePackageName(protoPackage);
this.swagger = swagger;
}
public Proto convert() {
convertDefinitions();
convertOperations();
for (; ; ) {
List<Runnable> oldPending = pending;
pending = new ArrayList<>();
for (Runnable runnable : oldPending) {
runnable.run();
}
if (pending.isEmpty()) {
break;
}
}
return createProto();
}
public static String escapePackageName(String name) {
return name.replaceAll("[\\-\\:]", "_");
}
public static String escapeMessageName(String name) {
return name.replaceAll("\\.", "_");
}
public static boolean isValidEnum(String name) {
if (name.contains(".") || name.contains("-")) {
return false;
}
return true;
}
private void convertDefinitions() {
if (swagger.getDefinitions() == null) {
return;
}
for (Entry<String, Model> entry : swagger.getDefinitions().entrySet()) {
convertDefinition(entry.getKey(), (ModelImpl) entry.getValue());
}
}
@SuppressWarnings("unchecked")
private void convertDefinition(String modelName, ModelImpl model) {
Map<String, Property> properties = model.getProperties();
if (properties == null) {
// it's a empty message
properties = Collections.emptyMap();
}
createMessage(modelName, (Map<String, Object>) (Object) properties);
}
private void createMessage(String protoName, Map<String, Object> properties, String... annotations) {
if (!messages.add(protoName)) {
// already created
return;
}
for (String annotation : annotations) {
msgStringBuilder.append("//");
appendLine(msgStringBuilder, annotation);
}
appendLine(msgStringBuilder, "message %s {", protoName);
int tag = 1;
for (Entry<String, Object> entry : properties.entrySet()) {
Object property = entry.getValue();
String propertyType = convertSwaggerType(property);
appendLine(msgStringBuilder, " %s %s = %d;", propertyType, entry.getKey(), tag);
tag++;
}
appendLine(msgStringBuilder, "}");
}
private void addImports(Proto proto) {
imports.add(proto.getFilename());
for (Message message : proto.getMessages()) {
messages.add(message.getCanonicalName());
}
}
private String convertSwaggerType(Object swaggerType) {
if (swaggerType == null) {
// void
addImports(ProtoConst.EMPTY_PROTO);
return ProtoConst.EMPTY.getCanonicalName();
}
SwaggerTypeAdapter adapter = SwaggerTypeAdapter.create(swaggerType);
String type = tryFindEnumType(adapter.getEnum());
if (type != null) {
return type;
}
type = findBaseType(adapter.getType(), adapter.getFormat());
if (type != null) {
return type;
}
type = adapter.getRefType();
if (type != null) {
return type;
}
Property itemProperty = adapter.getArrayItem();
if (itemProperty != null) {
return "repeated " + convertArrayOrMapItem(itemProperty);
}
itemProperty = adapter.getMapItem();
if (itemProperty != null) {
return String.format("map<string, %s>", convertArrayOrMapItem(itemProperty));
}
if (adapter.isJavaLangObject()) {
addImports(ProtoConst.ANY_PROTO);
return ProtoConst.ANY.getCanonicalName();
}
throw new IllegalStateException(String
.format("not support swagger type, class=%s, content=%s.", swaggerType.getClass().getName(),
Json.encode(swaggerType)));
}
private String convertArrayOrMapItem(Property itemProperty) {
SwaggerTypeAdapter itemAdapter = SwaggerTypeAdapter.create(itemProperty);
// List<List<>>, need to wrap
if (itemAdapter.getArrayItem() != null) {
String protoName = generateWrapPropertyName(List.class.getSimpleName(), itemAdapter.getArrayItem());
pending.add(() -> wrapPropertyToMessage(protoName, itemProperty));
return protoName;
}
// List<Map<>>, need to wrap
if (itemAdapter.getMapItem() != null) {
String protoName = generateWrapPropertyName(Map.class.getSimpleName(), itemAdapter.getMapItem());
pending.add(() -> wrapPropertyToMessage(protoName, itemProperty));
return protoName;
}
return convertSwaggerType(itemProperty);
}
private String generateWrapPropertyName(String prefix, Property property) {
SwaggerTypeAdapter adapter = SwaggerTypeAdapter.create(property);
// List<List<>>, need to wrap
if (adapter.getArrayItem() != null) {
return generateWrapPropertyName(prefix + List.class.getSimpleName(), adapter.getArrayItem());
}
// List<Map<>>, need to wrap
if (adapter.getMapItem() != null) {
return generateWrapPropertyName(prefix + Map.class.getSimpleName(), adapter.getMapItem());
}
// message name cannot have . (package separator)
return prefix + StringUtils.capitalize(escapeMessageName(convertSwaggerType(adapter)));
}
private void wrapPropertyToMessage(String protoName, Object property) {
createMessage(protoName, Collections.singletonMap("value", property), ProtoConst.ANNOTATION_WRAP_PROPERTY);
}
private String tryFindEnumType(List<String> enums) {
if (enums != null && !enums.isEmpty()) {
String strEnums = enums.toString();
String enumName = "Enum_" + Hashing.sha256().hashString(strEnums, StandardCharsets.UTF_8);
pending.add(() -> createEnum(enumName, enums));
return enumName;
}
return null;
}
private void createEnum(String enumName, List<String> enums) {
if (!messages.add(enumName)) {
// already created
return;
}
appendLine(msgStringBuilder, "enum %s {", enumName);
for (int idx = 0; idx < enums.size(); idx++) {
if (isValidEnum(enums.get(idx))) {
appendLine(msgStringBuilder, " %s =%d;", enums.get(idx), idx);
} else {
throw new IllegalStateException(
String.format("enum class [%s] name [%s] not supported by protobuffer.", enumName, enums.get(idx)));
}
}
appendLine(msgStringBuilder, "}");
}
private String findBaseType(String swaggerType, String swaggerFmt) {
String key = swaggerType + ":" + swaggerFmt;
switch (key) {
case "boolean:null":
return "bool";
// there is no int8/int16 in protobuf
case "integer:null":
return "int64";
case "integer:int8":
case "integer:int16":
case "integer:int32":
return "int32";
case "integer:int64":
return "int64";
case "number:null":
return "double";
case "number:float":
return "float";
case "number:double":
return "double";
case "string:null":
return "string";
case "string:byte":
return "bytes";
case "string:date": // LocalDate
case "string:date-time": // Date
return "int64";
case "file:null":
throw new IllegalStateException("not support swagger type: " + swaggerType);
}
return null;
}
private void convertOperations() {
Map<String, Path> paths = swagger.getPaths();
if (paths == null || paths.isEmpty()) {
return;
}
appendLine(serviceBuilder, "service MainService {");
for (Path path : paths.values()) {
for (Operation operation : path.getOperationMap().values()) {
if (isUpload(operation)) {
LOGGER.warn("Not support operation for highway {}.{}, {}", this.protoPackage, operation.getOperationId(),
"file upload not supported");
continue;
} else if (isDownload(operation)) {
LOGGER.warn("Not support operation for highway {}.{}, {}", this.protoPackage, operation.getOperationId(),
"file download not supported");
continue;
}
try {
convertOperation(operation);
} catch (Exception e) {
LOGGER.error("Not support operation for highway {}.{}", this.protoPackage, operation.getOperationId(), e);
}
}
}
serviceBuilder.setLength(serviceBuilder.length() - 1);
appendLine(serviceBuilder, "}");
}
private boolean isUpload(Operation operation) {
if (operation.getConsumes() != null && operation.getConsumes().contains(MediaType.MULTIPART_FORM_DATA)) {
return true;
}
return false;
}
private boolean isDownload(Operation operation) {
if (operation.getResponses().get("200").getResponseSchema() instanceof ModelImpl) {
ModelImpl model = (ModelImpl) operation.getResponses().get("200").getResponseSchema();
if ("file".equals(model.getType())) {
return true;
}
}
return false;
}
private void convertOperation(Operation operation) {
ProtoMethod protoMethod = new ProtoMethod();
fillRequestType(operation, protoMethod);
fillResponseType(operation, protoMethod);
appendLine(serviceBuilder, " //%s%s", ProtoConst.ANNOTATION_RPC, Json.encode(protoMethod));
appendLine(serviceBuilder, " rpc %s (%s) returns (%s);\n", operation.getOperationId(),
protoMethod.getArgTypeName(),
protoMethod.findResponse(Status.OK.getStatusCode()).getTypeName());
}
private void fillRequestType(Operation operation, ProtoMethod protoMethod) {
List<Parameter> parameters = operation.getParameters();
if (parameters.isEmpty()) {
addImports(ProtoConst.EMPTY_PROTO);
protoMethod.setArgTypeName(ProtoConst.EMPTY.getCanonicalName());
return;
}
if (parameters.size() == 1) {
String type = convertSwaggerType(parameters.get(0));
if (messages.contains(type)) {
protoMethod.setArgTypeName(type);
return;
}
}
String wrapName = StringUtils.capitalize(operation.getOperationId()) + "RequestWrap";
createWrapArgs(wrapName, parameters);
protoMethod.setArgTypeName(wrapName);
}
private void fillResponseType(Operation operation, ProtoMethod protoMethod) {
for (Entry<String, Response> entry : operation.getResponses().entrySet()) {
String type = convertSwaggerType(entry.getValue().getResponseSchema());
boolean wrapped = !messages.contains(type);
ProtoResponse protoResponse = new ProtoResponse();
protoResponse.setTypeName(type);
if (wrapped) {
String wrapName = StringUtils.capitalize(operation.getOperationId()) + "ResponseWrap" + entry.getKey();
wrapPropertyToMessage(wrapName, entry.getValue().getResponseSchema());
protoResponse.setTypeName(wrapName);
}
protoMethod.addResponse(entry.getKey(), protoResponse);
}
}
private void createWrapArgs(String wrapName, List<Parameter> parameters) {
Map<String, Object> properties = new LinkedHashMap<>();
for (Parameter parameter : parameters) {
properties.put(parameter.getName(), parameter);
}
createMessage(wrapName, properties, ProtoConst.ANNOTATION_WRAP_ARGUMENTS);
}
protected Proto createProto() {
StringBuilder sb = new StringBuilder();
appendLine(sb, "syntax = \"proto3\";");
for (String importMsg : imports) {
appendLine(sb, "import \"%s\";", importMsg);
}
if (StringUtils.isNotEmpty(protoPackage)) {
sb.append("package ").append(protoPackage).append(";\n");
}
sb.append(msgStringBuilder);
sb.append(serviceBuilder);
ProtoParser protoParser = new ProtoParser();
return protoParser.parseFromContent(sb.toString());
}
}