/* * Copyright 2017 LINE Corporation * * LINE Corporation 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 com.linecorp.armeria.server.grpc; import static com.google.common.collect.ImmutableMap.toImmutableMap; import java.io.IOException; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.protobuf.DescriptorProtos.DescriptorProto; import com.google.protobuf.DescriptorProtos.EnumDescriptorProto; import com.google.protobuf.DescriptorProtos.FieldDescriptorProto; import com.google.protobuf.DescriptorProtos.FileDescriptorProto; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto; import com.linecorp.armeria.server.docs.DocStringExtractor; /** * A DocString extractor for GRPC proto compiled descriptors. * * <p>To include docstrings in {@link com.linecorp.armeria.server.docs.DocService} pages, * configure the protobuf compiler to generate a descriptor set with source info and all imports * included. Place the descriptor set in the classpath location {@code META-INF/armeria/grpc} and ensure * the file extension is '.dsc'. The classpath location can be changed by setting the * {@code com.linecorp.armeria.grpc.descriptorDir} system property. * * <p>For example, to generate a descriptor set in gradle: * <pre>{@code * * protobuf { * generateProtoTasks { * all().each { task -> * task.generateDescriptorSet = true * task.descriptorSetOptions.includeSourceInfo = true * task.descriptorSetOptions.includeImports = true * task.descriptorSetOptions.path = * "${buildDir}/resources/main/META-INF/armeria/grpc/service-name.dsc" * } * } * } * * }</pre> */ final class GrpcDocStringExtractor extends DocStringExtractor { private static final Logger logger = LoggerFactory.getLogger(GrpcDocStringExtractor.class); GrpcDocStringExtractor() { super("META-INF/armeria/grpc", "com.linecorp.armeria.grpc.descriptorDir"); } @Override protected boolean acceptFile(String filename) { return filename.endsWith(".dsc"); } @Override protected Map<String, String> getDocStringsFromFiles(Map<String, byte[]> files) { return files.entrySet().stream() .flatMap(entry -> { try { FileDescriptorSet descriptors = FileDescriptorSet.parseFrom(entry.getValue()); return descriptors.getFileList().stream(); } catch (IOException e) { logger.info("Could not parse file at '{}', skipping. " + "Is the file a protobuf descriptor file?", entry.getKey()); return Stream.empty(); } }) .flatMap(f -> parseFile(f).entrySet().stream()) .collect(toImmutableMap(Entry::getKey, Entry::getValue)); } private static Map<String, String> parseFile(FileDescriptorProto descriptor) { return descriptor.getSourceCodeInfo().getLocationList().stream() .filter(l -> !l.getLeadingComments().isEmpty()) .map(l -> getFullName(descriptor, l.getPathList()) .map(fullName -> new SimpleImmutableEntry<>(fullName, l.getLeadingComments()))) .filter(Optional::isPresent) .map(Optional::get) .collect(toImmutableMap(Entry::getKey, Entry::getValue)); } // A path is field number and indices within a list of types, going through a tree of protobuf // descriptors. For example, the 2nd field of the 3rd nested message in the 1st message in a file // would have path [MESSAGE_TYPE_FIELD_NUMBER, 0, NESTED_TYPE_FIELD_NUMBER, 2, FIELD_FIELD_NUMBER, 1] private static Optional<String> getFullName(FileDescriptorProto descriptor, List<Integer> path) { String fullNameSoFar = descriptor.getPackage(); switch (path.get(0)) { case FileDescriptorProto.MESSAGE_TYPE_FIELD_NUMBER: DescriptorProto message = descriptor.getMessageType(path.get(1)); return appendMessageToFullName(message, path, fullNameSoFar); case FileDescriptorProto.ENUM_TYPE_FIELD_NUMBER: EnumDescriptorProto enumDescriptor = descriptor.getEnumType(path.get(1)); return Optional.of(appendEnumToFullName(enumDescriptor, path, fullNameSoFar)); case FileDescriptorProto.SERVICE_FIELD_NUMBER: ServiceDescriptorProto serviceDescriptor = descriptor.getService(path.get(1)); fullNameSoFar = appendNameComponent(fullNameSoFar, serviceDescriptor.getName()); if (path.size() > 2) { fullNameSoFar = appendFieldComponent( fullNameSoFar, serviceDescriptor.getMethod(path.get(3)).getName()); } return Optional.of(fullNameSoFar); default: return Optional.empty(); } } private static Optional<String> appendToFullName( DescriptorProto messageDescriptor, List<Integer> path, String fullNameSoFar) { switch (path.get(0)) { case DescriptorProto.NESTED_TYPE_FIELD_NUMBER: DescriptorProto nestedMessage = messageDescriptor.getNestedType(path.get(1)); return appendMessageToFullName(nestedMessage, path, fullNameSoFar); case DescriptorProto.ENUM_TYPE_FIELD_NUMBER: EnumDescriptorProto enumDescriptor = messageDescriptor.getEnumType(path.get(1)); return Optional.of(appendEnumToFullName(enumDescriptor, path, fullNameSoFar)); case DescriptorProto.FIELD_FIELD_NUMBER: FieldDescriptorProto fieldDescriptor = messageDescriptor.getField(path.get(1)); return Optional.of(appendFieldComponent(fullNameSoFar, fieldDescriptor.getName())); default: return Optional.empty(); } } private static Optional<String> appendMessageToFullName( DescriptorProto message, List<Integer> path, String fullNameSoFar) { fullNameSoFar = appendNameComponent(fullNameSoFar, message.getName()); return path.size() > 2 ? appendToFullName(message, path.subList(2, path.size()), fullNameSoFar) : Optional.of(fullNameSoFar); } private static String appendEnumToFullName( EnumDescriptorProto enumDescriptor, List<Integer> path, String fullNameSoFar) { fullNameSoFar = appendNameComponent(fullNameSoFar, enumDescriptor.getName()); if (path.size() > 2) { fullNameSoFar = appendFieldComponent(fullNameSoFar, enumDescriptor.getValue(path.get(3)).getName()); } return fullNameSoFar; } private static String appendNameComponent(String nameSoFar, String component) { return nameSoFar + "." + component; } private static String appendFieldComponent(String nameSoFar, String component) { return nameSoFar + "/" + component; } }