private static ServiceInfo buildHttpServiceInfo(String serviceName,
                                                    Collection<HttpEndpoint> endpoints) {
        final Multimap<String, HttpEndpoint> byMethodName = HashMultimap.create();
        endpoints.stream()
                 // Order by gRPC method name and the specified order of HTTP endpoints.
                 .sorted(Comparator.comparing(
                         httpEndpoint -> httpEndpoint.spec().methodName() + httpEndpoint.spec().order()))
                 // Group by gRPC method name and HTTP method.
                 .forEach(entry -> byMethodName.put(entry.spec().methodName() + '/' + entry.httpMethod(),
                                                    entry));

        final ImmutableList.Builder<MethodInfo> methodInfos = ImmutableList.builder();
        byMethodName.asMap().forEach((name, httpEndpoints) -> {
            final List<HttpEndpoint> sortedEndpoints =
                    httpEndpoints.stream().sorted(Comparator.comparingInt(ep -> ep.spec.order()))
                                 .collect(toImmutableList());

            final HttpEndpoint firstEndpoint = sortedEndpoints.get(0);
            final HttpEndpointSpecification firstSpec = firstEndpoint.spec();

            final ImmutableList.Builder<FieldInfo> fieldInfosBuilder = ImmutableList.builder();
            firstSpec.pathVariables().forEach(paramName -> {
                @Nullable
                final Parameter parameter = firstSpec.parameters().get(paramName);
                // It is possible not to have a gRPC field mapped by a path variable.
                // In this case, we treat the type of the path variable as 'String'.
                final TypeSignature typeSignature =
                        parameter != null ? toTypeSignature(parameter)
                                          : TypeSignature.ofBase(JavaType.STRING.name());
                fieldInfosBuilder.add(FieldInfo.builder(paramName, typeSignature)
                                               .requirement(FieldRequirement.REQUIRED)
                                               .location(FieldLocation.PATH)
                                               .build());
            });
            final String bodyParamName = firstSpec.httpRule().getBody();
            final FieldLocation fieldLocation = Strings.isNullOrEmpty(bodyParamName) ? FieldLocation.QUERY
                                                                                     : FieldLocation.BODY;
            firstSpec.parameters().forEach((paramName, parameter) -> {
                if (!firstSpec.pathVariables().contains(paramName)) {
                    final FieldInfoBuilder builder;
                    if (fieldLocation == FieldLocation.BODY && !"*".equals(bodyParamName) &&
                        paramName.startsWith(bodyParamName + '.')) {
                        builder = FieldInfo.builder(paramName.substring(bodyParamName.length() + 1),
                                                    toTypeSignature(parameter));
                    } else {
                        builder = FieldInfo.builder(paramName, toTypeSignature(parameter));
                    }

                    builder.requirement(parameter.isRequired() ? FieldRequirement.REQUIRED
                                                               : FieldRequirement.OPTIONAL);

                    fieldInfosBuilder.add(builder.location(fieldLocation)
                                                 .build());
                }
            });

            final List<EndpointInfo> endpointInfos =
                    sortedEndpoints.stream()
                                   .map(httpEndpoint -> endpointInfoBuilder(
                                           httpEndpoint.spec().route(),
                                           httpEndpoint.config().virtualHost().hostnamePattern())
                                           // DocService client works only if the media type equals to
                                           // 'application/json; charset=utf-8'.
                                           .availableMimeTypes(MediaType.JSON_UTF_8)
                                           .build()).collect(toImmutableList());

            final List<String> examplePaths =
                    sortedEndpoints.stream()
                                   .map(httpEndpoint -> {
                                       final Route route = httpEndpoint.spec().route();
                                       if (route.pathType() == RoutePathType.REGEX ||
                                           route.pathType() == RoutePathType.REGEX_WITH_PREFIX) {
                                           return convertRegexPath(route.patternString());
                                       }
                                       return route.patternString();
                                   })
                                   .collect(toImmutableList());

            final List<String> exampleQueries =
                    sortedEndpoints.stream().map(httpEndpoint -> {
                        final HttpEndpointSpecification spec = httpEndpoint.spec();
                        return spec.parameters().entrySet().stream()
                                   // Exclude path parameters.
                                   .filter(entry -> !spec.pathVariables().contains(entry.getKey()))
                                   // Join all remaining parameters as a single query string.
                                   .map(p -> p.getKey() + '=' + p.getValue().type().name())
                                   .collect(Collectors.joining("&"));
                    }).filter(queries -> !queries.isEmpty()).collect(toImmutableList());

            methodInfos.add(new MethodInfo(
                    serviceName,
                    firstSpec.methodName(),
                    firstSpec.order(),
                    descriptiveMessageSignature(firstSpec.methodDescriptor().getOutputType()),
                    fieldInfosBuilder.build(),
                    endpointInfos,
                    examplePaths,
                    exampleQueries,
                    firstEndpoint.httpMethod(), DescriptionInfo.empty()));
        });
        return new ServiceInfo(serviceName, methodInfos.build());
    }
{@link DocServicePlugin} implementation that supports {@link GrpcService}s.
grpc/src/main/java/com/linecorp/armeria/internal/server/grpc/GrpcDocServicePlugin.java