All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fullcontact.rpc.jersey.CodeGenerator Maven / Gradle / Ivy

The newest version!
package com.fullcontact.rpc.jersey;

import com.fullcontact.rpc.jersey.util.ProtobufDescriptorJavaUtil;
import com.fullcontact.rpc.jersey.yaml.YamlHttpConfig;
import com.fullcontact.rpc.jersey.yaml.YamlHttpRule;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import com.google.api.AnnotationsProto;
import com.google.api.HttpRule;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CaseFormat;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.Descriptors;
import com.google.protobuf.compiler.PluginProtos;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.Builder;
import lombok.Value;

/**
 * Jersey JSON/proto REST gRPC gateway compiler
 *
 * For every gRPC method annotated with google.api.http options, this compiler emits a Jersey resource method.
 *
 * @author Michael Rose (xorlev)
 */
public class CodeGenerator {

    public static ImmutableList parsePathParams(Descriptors.Descriptor inputDescriptor,
            PathParser.ParsedPath path) {
        ImmutableList.Builder pathParams = ImmutableList.builder();
        path.visit(new PathParser.EmptySegmentVisitor() {
            @Override
            public void visit(PathParser.NamedVariable namedVariable) {
                ImmutableList fieldDescriptor =
                        ProtobufDescriptorJavaUtil.fieldPath(inputDescriptor, namedVariable.getName());

                if (fieldDescriptor.isEmpty()) {
                    throw new IllegalArgumentException("Couldn't find path param: " + namedVariable.getName()
                            + " in input type: " + inputDescriptor.toProto());
                }

                Descriptors.FieldDescriptor descriptor = Iterables.getLast(fieldDescriptor);

                if (descriptor.isMapField() || descriptor.isRepeated()) {
                    throw new IllegalArgumentException(
                            "Cannot map path param '" + namedVariable.getName() + "' as URL mapping is not supported " +
                                    "for map or repeated field types."
                    );
                }

                pathParams.add(new PathParam(namedVariable.getName(), fieldDescriptor));
            }
        });

        return pathParams.build();
    }

    public PluginProtos.CodeGeneratorResponse generate(PluginProtos.CodeGeneratorRequest request)
            throws Descriptors.DescriptorValidationException {
        Set options = Sets.newHashSet(Splitter.on(',').split(request.getParameter()));

        boolean isProxy = !options.contains("direct");

        Map lookup = new HashMap<>();
        PluginProtos.CodeGeneratorResponse.Builder response = PluginProtos.CodeGeneratorResponse.newBuilder();

        List fileDescriptors = Lists.newArrayList(
                DescriptorProtos.MethodOptions.getDescriptor().getFile(),
                AnnotationsProto.getDescriptor(),
                HttpRule.getDescriptor().getFile()
        );

        Optional yamlConfig = YamlHttpConfig.getFromOptions(options);

        for (DescriptorProtos.FileDescriptorProto fdProto : request.getProtoFileList()) {
            // Descriptors are provided in dependency-topological order
            // each time we collect a new FileDescriptor, we add it to a
            // mutable list of descriptors and append the entire dependency
            // chain to each new FileDescriptor to allow crossLink() to function.
            // TODO(xorlev): might have to be more selective about deps in future
            Descriptors.FileDescriptor fd = Descriptors.FileDescriptor.buildFrom(
                    fdProto, fileDescriptors.toArray(new Descriptors.FileDescriptor[]{})
            );
            fileDescriptors.add(fd);

            // if type starts with a ".", it's in this package
            // otherwise it's fully qualified
            String protoPackage = fdProto.getPackage();
            for (DescriptorProtos.DescriptorProto d : fdProto.getMessageTypeList()) {
                String prefix = ".";

                if (!Strings.isNullOrEmpty(protoPackage)) {
                    prefix += protoPackage + ".";
                }

                lookup.put(prefix + d.getName(), fd.findMessageTypeByName(d.getName()));
            }

            // Find RPC methods with HTTP extensions
            List methodsToGenerate = new ArrayList<>();
            for (Descriptors.ServiceDescriptor serviceDescriptor : fd.getServices()) {
                DescriptorProtos.ServiceDescriptorProto serviceDescriptorProto = serviceDescriptor.toProto();
                for (DescriptorProtos.MethodDescriptorProto methodProto : serviceDescriptorProto.getMethodList()) {
                    String fullMethodName = serviceDescriptor.getFullName() + "." + methodProto.getName();
                    if (yamlConfig.isPresent()) {   //Check to see if the rules are defined in the YAML
                        for (YamlHttpRule rule : yamlConfig.get().getRules()) {
                            if (rule.getSelector().equals(fullMethodName) || rule.getSelector()
                                    .equals("*")) { //TODO:  com.foo.*
                                DescriptorProtos.MethodOptions yamlOptions = DescriptorProtos.MethodOptions.newBuilder()
                                        .setExtension(AnnotationsProto.http, rule.buildHttpRule())
                                        .build();
                                methodProto = DescriptorProtos.MethodDescriptorProto.newBuilder()
                                        .mergeFrom(methodProto)
                                        .setOptions(yamlOptions)
                                        .build();
                            }
                        }
                    }
                    if (methodProto.getOptions().hasExtension(AnnotationsProto.http)) {
                        // TODO(xorlev): support server streaming
                        if (methodProto.getClientStreaming()) {
                            throw new IllegalArgumentException("grpc-jersey does not support client streaming");
                        }

                        methodsToGenerate.add(new ServiceAndMethod(serviceDescriptor, methodProto));
                    }
                }
            }
            if (!methodsToGenerate.isEmpty()) {
                generateResource(response, lookup, fdProto, methodsToGenerate, isProxy);
            }
        }

        return response.build();
    }

    private void generateResource(
            PluginProtos.CodeGeneratorResponse.Builder response,
            Map descriptorTable,
            DescriptorProtos.FileDescriptorProto fileDescriptorProto,
            List generate,
            boolean isProxy) {
        ResourceToGenerate r = buildResourceSpec(descriptorTable, fileDescriptorProto, generate, isProxy);

        MustacheFactory mf = new DefaultMustacheFactory();
        Mustache mustache = mf.compile("resource.tmpl.java");
        StringWriter writer = new StringWriter();
        mustache.execute(writer, r);

        response.addFile(PluginProtos.CodeGeneratorResponse.File.newBuilder()
                .setContent(writer.toString())
                .setName(r.getFileName())
                .build());

        System.err.println(writer.toString());
    }

    /**
     * Creates a generator spec for a given service resource and all of the methods.
     *
     * @param descriptorTable mapping of proto.path.MessageType to the descriptor instance
     * @param fileDescriptorProto file descriptor of the origin service
     * @param methodSpecs list of methods in the given service
     * @param isProxy should this resource use client stubs or implbase?
     */
    @VisibleForTesting
    ResourceToGenerate buildResourceSpec(
            Map descriptorTable,
            DescriptorProtos.FileDescriptorProto fileDescriptorProto,
            List methodSpecs,
            boolean isProxy) {
        Descriptors.ServiceDescriptor serviceDescriptor = methodSpecs.get(0).getServiceDescriptor();
        DescriptorProtos.ServiceDescriptorProto sdp = methodSpecs.get(0).getServiceDescriptor().toProto();
        String packageName = ProtobufDescriptorJavaUtil.javaPackage(fileDescriptorProto);
        String className = ProtobufDescriptorJavaUtil.jerseyResourceClassName(sdp);
        String grpcImplClass = (isProxy) ?
                ProtobufDescriptorJavaUtil.grpcStubClass(fileDescriptorProto, sdp) :
                ProtobufDescriptorJavaUtil.grpcImplBaseClass(fileDescriptorProto, sdp);
        String fileName = packageName.replace('.', '/') + "/" + className + ".java";

        ImmutableList.Builder methods = ImmutableList.builder();
        for (ServiceAndMethod sam : methodSpecs) {
            Descriptors.Descriptor inputDescriptor = descriptorTable.get(sam.getMethodDescriptor().getInputType());
            Descriptors.Descriptor outputDescriptor = descriptorTable.get(sam.getMethodDescriptor().getOutputType());
            List methodToGenerate = parseRule(sam, inputDescriptor, outputDescriptor);
            methods.addAll(methodToGenerate);
        }

        return ResourceToGenerate
                .builder()
                .serviceDescriptor(serviceDescriptor)
                .javaPackage(ProtobufDescriptorJavaUtil.javaPackage(fileDescriptorProto))
                .className(className)
                .grpcStub(grpcImplClass)
                .methods(methods.build())
                .isProxy(isProxy)
                .fileName(fileName)
                .build();
    }

    /**
     * Generates a {@link ResourceMethodToGenerate} spec from the {@link ServiceAndMethod} plus the input/output RPC
     * message descriptors
     *
     * @param sam named tuple of (service descriptor proto, method descriptor proto)
     * @param inputDescriptor RPC input descriptor
     * @param outputDescriptor RPC output descriptor
     * @return list of handlers to handle the given RPC method. Usually a single result, but can be multiple if
     * additional_bindings is defined.
     */
    @VisibleForTesting
    ImmutableList parseRule(ServiceAndMethod sam,
            Descriptors.Descriptor inputDescriptor,
            Descriptors.Descriptor outputDescriptor) {
        HttpRule baseRule = sam.getMethodDescriptor().getOptions().getExtension(AnnotationsProto.http);

        ImmutableList rules = ImmutableList.builder()
                .add(baseRule)
                .addAll(baseRule.getAdditionalBindingsList())
                .build();

        ImmutableList.Builder methodsToGenerate = ImmutableList.builder();
        int methodIndex = 0;
        for (HttpRule rule : rules) {
            String method = rule.getPatternCase().toString();
            String path = "";
            switch (rule.getPatternCase()) {
                case GET:
                    path = rule.getGet();
                    break;
                case PUT:
                    path = rule.getPut();
                    break;
                case POST:
                    path = rule.getPost();
                    break;
                case DELETE:
                    path = rule.getDelete();
                    break;
                case PATCH:
                    path = rule.getPatch();
                    break;
                case CUSTOM:
                    throw new IllegalArgumentException("Jersey compiler does not support custom HTTP verbs.");
                case PATTERN_NOT_SET:
                    throw new IllegalArgumentException("Pattern (GET,PUT,POST,DELETE,PATCH) must be set.");
            }

            if (path.trim().isEmpty()) {
                throw new IllegalArgumentException("rule path must be set");
            }

            // TODO(xorlev): check for URL overlap
            PathParser.ParsedPath parsedPath = PathParser.parse(path);
            ImmutableList pathParams = parsePathParams(inputDescriptor, parsedPath);

            String bodyFieldPath = Strings.emptyToNull(rule.getBody());

            if (bodyFieldPath != null && !bodyFieldPath.equals("*")) {
                ImmutableList fieldDescriptor =
                        ProtobufDescriptorJavaUtil.fieldPath(inputDescriptor, bodyFieldPath);

                if (fieldDescriptor.isEmpty()) {
                    List pathSegments = Splitter.on('.').omitEmptyStrings().trimResults().splitToList(path);

                    while (!pathSegments.isEmpty()) {
                        pathSegments.remove(pathSegments.size() - 1);

                        fieldDescriptor = ProtobufDescriptorJavaUtil.fieldPath(inputDescriptor, bodyFieldPath);

                        if (!fieldDescriptor.isEmpty()) {
                            // TODO: remove bodyFieldPath segments until we have a fieldDescriptor
                            List availableFields = Iterables.getLast(fieldDescriptor).getMessageType()
                                    .getFields()
                                    .stream()
                                    .map(Descriptors.FieldDescriptor::getName)
                                    .collect(Collectors.toList());
                            throw new IllegalArgumentException("'body' attribute refers to non-existent field " +
                                    "'" + bodyFieldPath + "'. Available fields: " +
                                    availableFields);
                        }
                    }
                }
            }

            methodsToGenerate.add(new ResourceMethodToGenerate(
                    sam.getMethodDescriptor().getName(),
                    method,
                    parsedPath.toPath(),
                    pathParams,
                    bodyFieldPath,
                    ProtobufDescriptorJavaUtil.genClassName(inputDescriptor),
                    ProtobufDescriptorJavaUtil.genClassName(outputDescriptor),
                    methodIndex++,
                    sam.getMethodDescriptor().hasClientStreaming(),
                    sam.getMethodDescriptor().hasServerStreaming()
            ));
        }

        return methodsToGenerate.build();
    }

    @Value
    @Builder
    static class ResourceToGenerate {
        Descriptors.ServiceDescriptor serviceDescriptor;
        String javaPackage;
        String className;
        String grpcStub; // fully-qualified class name;
        List methods;
        boolean isProxy;
        String fileName;

        String grpcJerseyVersion() {
            return Build.version();
        }

        String sourceProtoFile() {
            return serviceDescriptor.getFile().getName();
        }

        List unaryMethods() {
            return FluentIterable.from(methods).filter(m -> !m.isStreaming()).toList();
        }

        List streamMethods() {
            return FluentIterable.from(methods).filter(ResourceMethodToGenerate::isStreaming).toList();
        }
    }

    @Value
    static class PathParam {
        String name;
        ImmutableList fieldDescriptor;

        String nameSanitized() {
            return CharMatcher.javaLetterOrDigit().retainFrom(name);
        }

        List descriptorPath() {
            return fieldDescriptor.stream()
                    .map(Descriptors.FieldDescriptor::getName)
                    .collect(Collectors.toList());
        }

        String descriptorJoined() {
            return Joiner.on(',').join(descriptorPath());
        }
    }

    @Value
    static class ResourceMethodToGenerate {
        String methodName;
        String method; // GET, POST...
        String path;
        List pathParams;
        String bodyFieldPath;
        String requestType;
        String responseType;
        int methodIndex;
        boolean isClientStreaming;
        boolean isServerStreaming;

        String methodNameLower() {
            return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, methodName);
        }

        boolean isStreaming() {
            return isServerStreaming || isClientStreaming;
        }
    }

    /**
     * Named tuple of (service descriptor, method descriptor proto)
     */
    @Value
    static class ServiceAndMethod {
        Descriptors.ServiceDescriptor serviceDescriptor;
        DescriptorProtos.MethodDescriptorProto methodDescriptor;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy