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

com.tencent.trpc.codegen.TRpcCodeGenerator Maven / Gradle / Ivy

/*
 * Tencent is pleased to support the open source community by making tRPC available.
 *
 * Copyright (C) 2023 THL A29 Limited, a Tencent company. 
 * All rights reserved.
 *
 * If you have downloaded a copy of the tRPC source code from Tencent,
 * please note that tRPC source code is licensed under the Apache 2.0 License,
 * A copy of the Apache 2.0 License can be found in the LICENSE file.
 */

package com.tencent.trpc.codegen;

import com.google.protobuf.ApiOrBuilder;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.Descriptors;
import com.tencent.trpc.codegen.protobuf.FileDescriptorsCompiler;
import com.tencent.trpc.codegen.protobuf.ProtoParser;
import com.tencent.trpc.codegen.protobuf.ProtoSourceInfo;
import com.tencent.trpc.codegen.protoc.Language;
import com.tencent.trpc.codegen.protoc.Protoc;
import com.tencent.trpc.codegen.protoc.ProtocExecutionResult;
import com.tencent.trpc.codegen.protoc.ProtocInstruction;
import com.tencent.trpc.codegen.protoc.ProtocPlugin;
import com.tencent.trpc.codegen.protoc.ProtocPluginInstruction;
import com.tencent.trpc.codegen.template.CodeFileGenerator;
import com.tencent.trpc.codegen.util.JarUtils;
import io.envoyproxy.pgv.Validator;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * tRPC code generator. Responsible for generating protoc stub code and tRPC interfaces.
 */
public class TRpcCodeGenerator {

    private static final Logger log = LoggerFactory.getLogger(TRpcCodeGenerator.class);
    private static final String DEFAULT_DESCRIPTOR_FILENAME = "descriptor.pb";
    private static final String DEFAULT_TMP_OUTPUT_FOLDER = "generated";
    private static final String DEFAULT_TMP_IMPORT_FOLDER = "imports";
    /**
     * CodeFileGenerator for generating templated codes
     */
    private final CodeFileGenerator codeFileGenerator;
    /**
     * Transfer protocol (tRPC/gRPC)
     */
    private final Protocol protocol;
    /**
     * Code language
     */
    private final Language language;
    /**
     * Representing protoc executable file
     */
    private final Protoc protoc;
    /**
     * Directory of where the .proto files in
     */
    private final Path protoPath;
    /**
     * Directory of generated code files
     */
    private final Path outPath;
    /**
     * Temporary directory used during code generation
     */
    private final Path tmpPath;
    /**
     * Temporary directory for storing generated code files (must be subfolder of tmpPath)
     */
    private final Path tmpOutPath;
    /**
     * A hook for customizing code generation
     */
    private final TRpcCodeGeneratorHook codeGeneratorHook;

    private TRpcCodeGenerator(CodeFileGenerator codeFileGenerator,
            Protocol protocol,
            Language language,
            Protoc protoc,
            CodeGeneratorPath codeGeneratorPath,
            TRpcCodeGeneratorHook codeGeneratorHook) {
        this.codeFileGenerator = codeFileGenerator;
        this.protocol = protocol;
        this.language = language;
        this.protoc = protoc;
        this.protoPath = codeGeneratorPath.getProtoPath();
        this.outPath = codeGeneratorPath.getOutPath();
        this.tmpPath = codeGeneratorPath.getTmpPath();
        this.codeGeneratorHook = codeGeneratorHook;
        this.tmpOutPath = tmpPath.resolve(DEFAULT_TMP_OUTPUT_FOLDER);
    }

    /**
     * Run the code generation process
     *
     * @throws TRpcCodeGenerateException if error occurred while generating code
     */
    public void generateCode() throws TRpcCodeGenerateException {
        log.info("generating code... language={}, protoPath={}, outPath={}, protoc={}",
                language, protoPath, outPath, protoc);
        try {
            Files.createDirectories(tmpPath);
            List importPaths = prepareImportPaths();
            Path descriptorFile = generateDescriptorFile(getProtoFiles(true), importPaths);
            Files.createDirectories(tmpOutPath);
            List fdList = compileDescriptorSet(descriptorFile);
            Map customVariables = codeGeneratorHook.getCustomVariables(fdList);
            ProtoSourceInfo protoSourceInfo = ProtoParser.parseFileDescriptors(fdList);
            codeFileGenerator.generateCodeFiles(protocol, protoSourceInfo, tmpOutPath, customVariables);
            generateStub(getProtoFiles(false), importPaths, protoSourceInfo.isUsingValidator());
            assembleOutputs();
            log.info("code generate complete, exported to {}", outPath);
        } catch (IOException e) {
            throw new TRpcCodeGenerateException("IOException", e);
        } finally {
            FileUtils.deleteQuietly(tmpPath.toFile());
        }
    }

    /**
     * Prepare the directories contain global proto dependencies(imported .proto files).
     * By default, the returned directories will contain trpc.proto and google/protobuf/**.proto
     *
     * @return the imported directories
     */
    private List prepareImportPaths() throws IOException {
        List importPaths = new ArrayList<>();
        importPaths.add(prepareProtoDependencies());
        List additionalDependencies = codeGeneratorHook.getAdditionalProtoDependencyPaths();
        if (additionalDependencies != null) {
            importPaths.addAll(additionalDependencies);
        }
        return importPaths;
    }

    private Path prepareProtoDependencies() throws IOException {
        Path importPath = tmpPath.resolve(DEFAULT_TMP_IMPORT_FOLDER);
        Files.createDirectories(importPath);
        Path filePath = importPath.resolve("trpc.proto");
        try (InputStream trpcProto = getClass().getClassLoader().getResourceAsStream("imports/trpc.proto");
                OutputStream out = Files.newOutputStream(filePath)) {
            if (trpcProto == null) {
                // normally shouldn't happen
                throw new TRpcCodeGenerateException("cannot find 'imports/trpc.proto' in classpath");
            }
            IOUtils.copy(trpcProto, out);
            extractCommonProtoFiles(importPath);
        } catch (IOException | URISyntaxException e) {
            throw new TRpcCodeGenerateException("extract common .proto files failed", e);
        }
        return importPath;
    }

    private void extractCommonProtoFiles(Path outputPath) throws URISyntaxException, IOException {
        // extract google/protobuf/*.proto from protobuf-java.jar
        JarFile jar = new JarFile(new File(
                ApiOrBuilder.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()));
        JarUtils.extractJarFolder(jar, "google/", outputPath);

        // extract validate.proto from pgv-java-stub.jar
        jar = new JarFile(
                new File(Validator.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()));
        JarUtils.extractFileFromJar(jar, new JarEntry("validate/validate.proto"),
                outputPath.resolve("validate.proto"));
    }

    /**
     * Get .proto filenames under protoPath.
     *
     * @param shallow true: only get .proto files directly under protoPath.
     *         false: traverse protoPath recursively to get all .proto files
     * @return a list of .proto filenames, including subfolder names
     */
    private List getProtoFiles(boolean shallow) throws IOException {
        List protoFiles = walkDirectoryForProtoFiles(protoPath, shallow).stream()
                .map(protoPath::relativize)
                .map(Path::toString)
                .collect(Collectors.toList());
        if (protoFiles.isEmpty()) {
            throw new TRpcCodeGenerateException("no .proto files found in " + protoPath);
        }
        return protoFiles;
    }

    private List walkDirectoryForProtoFiles(Path directory, boolean shallow) throws IOException {
        List protoFiles = new ArrayList<>();
        try (DirectoryStream directoryStream = Files.newDirectoryStream(directory)) {
            for (Path path : directoryStream) {
                if (!shallow && Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
                    protoFiles.addAll(walkDirectoryForProtoFiles(path, false));
                } else if (path.getFileName().toString().endsWith(".proto") && Files.isRegularFile(path)) {
                    protoFiles.add(path);
                }
            }
        }
        return protoFiles;
    }

    /**
     * Generate protoc descriptor set file.
     *
     * @param protoFiles list of .proto filenames (relative to protoPath)
     * @param importPaths list of import directories
     * @return {@code Path} of the descriptor set file
     */
    private Path generateDescriptorFile(List protoFiles, List importPaths) {
        log.info("generating descriptor set for {}", protoFiles);
        Path descriptorPath = tmpPath.resolve(DEFAULT_DESCRIPTOR_FILENAME);
        ProtocExecutionResult result = protoc.generateDescriptorSet(ProtocInstruction.builder()
                .sourceDirectory(protoPath)
                .sourceFiles(protoFiles)
                .importPaths(importPaths)
                .output(descriptorPath)
                .build());
        if (!result.isSuccess()) {
            throw new TRpcCodeGenerateException("generate descriptor set failed: " + result, result.getCause());
        }
        return descriptorPath;
    }

    private List compileDescriptorSet(Path descriptorPath) throws IOException {
        DescriptorProtos.FileDescriptorSet set = ProtoParser.parseDescriptorSetFile(descriptorPath);
        FileDescriptorsCompiler compiler = new FileDescriptorsCompiler(set.getFileList());
        return compiler.compile();
    }

    /**
     * invoke protoc to generate stub codes. Generated code files are placed under tmpOutPath
     *
     * @param protoFiles list of .proto filenames (relative to protoPath)
     * @param importPaths list of import directories
     * @param withValidatePlugin if protoc-gen-validate plugin is required
     */
    private void generateStub(List protoFiles,
            List importPaths,
            boolean withValidatePlugin) {
        log.info("generating protobuf stub code for {}", protoFiles);
        ProtocInstruction.ProtocInstructionBuilder instructionBuilder = ProtocInstruction.builder()
                .sourceDirectory(protoPath)
                .sourceFiles(protoFiles)
                .importPaths(importPaths)
                .language(language)
                .output(tmpOutPath);
        if (withValidatePlugin) {
            instructionBuilder.pluginInstruction(new ProtocPluginInstruction(ProtocPlugin.GEN_VALIDATE_PLUGIN_NAME,
                    Collections.singletonList("lang=" + language.toString().toLowerCase()), tmpOutPath));
        }
        ProtocExecutionResult result = protoc.generateStub(instructionBuilder.build());
        if (!result.isSuccess()) {
            throw new TRpcCodeGenerateException("generate stub failed: " + result, result.getCause());
        }
    }

    /**
     * assemble generated code files, copy them to outPath.
     */
    private void assembleOutputs() throws IOException {
        log.info("assembling outputs...");
        Files.createDirectories(outPath);
        try (DirectoryStream directoryStream = Files.newDirectoryStream(tmpOutPath)) {
            for (Path path : directoryStream) {
                if (Files.isDirectory(path)) {
                    FileUtils.copyDirectoryToDirectory(path.toFile(), outPath.toFile());
                } else {
                    FileUtils.copyFileToDirectory(path.toFile(), outPath.toFile());
                }
            }
        }
    }

    /**
     * Builder for {@link TRpcCodeGenerator}
     *
     * @return Builder for {@link TRpcCodeGenerator}
     */
    public static TRpcCodeGeneratorBuilder builder() {
        return new TRpcCodeGeneratorBuilder();
    }


    public static final class TRpcCodeGeneratorBuilder {

        private CodeFileGenerator codeFileGenerator;
        private Protocol protocol = Protocol.TRPC;
        private Language language = Language.JAVA;
        private Protoc protoc;
        private Path protoPath;
        private Path outPath;
        private Path tmpPath;
        private TRpcCodeGeneratorHook codeGeneratorHook;

        private TRpcCodeGeneratorBuilder() {
        }

        /**
         * Set {@link CodeFileGenerator} used for generating templated codes.
         */
        public TRpcCodeGeneratorBuilder codeFileGenerator(CodeFileGenerator codeFileGenerator) {
            this.codeFileGenerator = codeFileGenerator;
            return this;
        }

        /**
         * Set {@link Protocol}, default is TRPC.
         */
        public TRpcCodeGeneratorBuilder protocol(Protocol protocol) {
            this.protocol = protocol;
            return this;
        }

        /**
         * Set {@link Language}, default is JAVA.
         */
        public TRpcCodeGeneratorBuilder language(Language language) {
            this.language = language;
            return this;
        }

        /**
         * Set {@link Protoc} representing an executable protoc binary file.
         */
        public TRpcCodeGeneratorBuilder protoc(Protoc protoc) {
            this.protoc = protoc;
            return this;
        }

        /**
         * Set .proto files directory
         */
        public TRpcCodeGeneratorBuilder protoPath(Path protoPath) {
            this.protoPath = protoPath;
            return this;
        }

        /**
         * Set code output directory
         */
        public TRpcCodeGeneratorBuilder outPath(Path outPath) {
            this.outPath = outPath;
            return this;
        }

        /**
         * Set temporary working directory, a randomly named directory will be used if not specified
         */
        public TRpcCodeGeneratorBuilder tmpPath(Path tmpPath) {
            this.tmpPath = tmpPath;
            return this;
        }

        /**
         * Set the {@link TRpcCodeGeneratorHook} for customizing.
         * {@link DefaultCodeGeneratorHook} will be used if not specified.
         */
        public TRpcCodeGeneratorBuilder codeGeneratorHook(TRpcCodeGeneratorHook codeGeneratorHook) {
            this.codeGeneratorHook = codeGeneratorHook;
            return this;
        }

        /**
         * Build {@link TRpcCodeGenerator}
         */
        public TRpcCodeGenerator build() {
            if (codeFileGenerator == null) {
                throw new IllegalArgumentException("codeFileGenerator must not be null");
            }
            if (protoc == null) {
                throw new IllegalArgumentException("protoc must not be null");
            }
            if (protoPath == null) {
                throw new IllegalArgumentException("protoPath must not be null");
            }
            if (outPath == null) {
                throw new IllegalArgumentException("outPath must not be null");
            }
            if (tmpPath == null) {
                tmpPath = protoPath.getParent().resolve("tmp" + RandomStringUtils.randomAlphanumeric(6));
            }
            if (codeGeneratorHook == null) {
                codeGeneratorHook = new DefaultCodeGeneratorHook();
            }
            CodeGeneratorPath codeGeneratorPath = new CodeGeneratorPath(protoPath, outPath, tmpPath);
            return new TRpcCodeGenerator(codeFileGenerator, protocol, language, protoc, codeGeneratorPath,
                    codeGeneratorHook);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy