org.xolstice.maven.plugin.protobuf.Protoc.java Source code

Java tutorial

Introduction

Here is the source code for org.xolstice.maven.plugin.protobuf.Protoc.java

Source

package org.xolstice.maven.plugin.protobuf;

/*
 * Copyright (c) 2016 Maven Protocol Buffers Plugin Authors. All rights reserved.
 *
 * Licensed 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.
 */

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.apache.maven.plugin.logging.Log;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.CommandLineUtils.StringStreamConsumer;
import org.codehaus.plexus.util.cli.Commandline;

import java.io.File;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Lists.newLinkedList;

/**
 * This class represents an invokable configuration of the {@code protoc} compiler.
 * The actual executable is invoked using the plexus {@link Commandline}.
 */
final class Protoc {

    /**
     * Prefix for logging the debug messages.
     */
    private static final String LOG_PREFIX = "[PROTOC] ";

    /**
     * Path to the {@code protoc} executable.
     */
    private final String executable;

    /**
     * A set of directories in which to search for definition imports.
     */
    private final ImmutableSet<File> protoPathElements;

    /**
     * A set of protobuf definitions to process.
     */
    private final ImmutableSet<File> protoFiles;

    /**
     * A directory into which Java source files will be generated.
     */
    private final File javaOutputDirectory;

    /**
     * A directory into which JavaNano source files will be generated.
     */
    private final File javaNanoOutputDirectory;

    private final ImmutableSet<ProtocPlugin> plugins;

    private final File pluginDirectory;

    private final String nativePluginId;

    private final String nativePluginExecutable;

    private final String nativePluginParameter;

    /**
     * A directory into which C++ source files will be generated.
     */
    private final File cppOutputDirectory;

    /**
     * A directory into which Python source files will be generated.
     */
    private final File pythonOutputDirectory;

    /**
     *  A directory into which a custom protoc plugin will generate files.
     */
    private final File customOutputDirectory;

    private final File descriptorSetFile;

    private final boolean includeImportsInDescriptorSet;

    private final boolean includeSourceInfoInDescriptorSet;

    /**
     * A buffer to consume standard output from the {@code protoc} executable.
     */
    private final StringStreamConsumer output;

    /**
     * A buffer to consume error output from the {@code protoc} executable.
     */
    private final StringStreamConsumer error;

    /**
     * Constructs a new instance. This should only be used by the {@link Builder}.
     *
     * @param executable path to the {@code protoc} executable.
     * @param protoPath a set of directories in which to search for definition imports.
     * @param protoFiles a set of protobuf definitions to process.
     * @param javaOutputDirectory a directory into which Java source files will be generated.
     * @param javaNanoOutputDirectory a directory into which JavaNano source files will be generated.
     * @param cppOutputDirectory a directory into which C++ source files will be generated.
     * @param pythonOutputDirectory a directory into which Python source files will be generated.
     * @param customOutputDirectory a directory into which a custom protoc plugin will generate files.
     * @param descriptorSetFile The directory into which a descriptor set will be generated;
     *                          if {@code null}, no descriptor set will be written
     * @param includeImportsInDescriptorSet If {@code true}, dependencies will be included in the descriptor set.
     * @param includeSourceInfoInDescriptorSet If {@code true}, source code information will be included 
     *                                         in the descriptor set.
     * @param plugins a set of java protoc plugins.
     * @param pluginDirectory location of protoc plugins to be added to system path.
     * @param nativePluginId a unique id of a native plugin.
     * @param nativePluginExecutable path to the native plugin executable.
     * @param nativePluginParameter an optional parameter for a native plugin.
     */
    private Protoc(final String executable, final ImmutableSet<File> protoPath, final ImmutableSet<File> protoFiles,
            final File javaOutputDirectory, final File javaNanoOutputDirectory, final File cppOutputDirectory,
            final File pythonOutputDirectory, final File customOutputDirectory, final File descriptorSetFile,
            final boolean includeImportsInDescriptorSet, final boolean includeSourceInfoInDescriptorSet,
            final ImmutableSet<ProtocPlugin> plugins, final File pluginDirectory, final String nativePluginId,
            final String nativePluginExecutable, final String nativePluginParameter) {
        this.executable = checkNotNull(executable, "executable");
        this.protoPathElements = checkNotNull(protoPath, "protoPath");
        this.protoFiles = checkNotNull(protoFiles, "protoFiles");
        this.javaOutputDirectory = javaOutputDirectory;
        this.javaNanoOutputDirectory = javaNanoOutputDirectory;
        this.cppOutputDirectory = cppOutputDirectory;
        this.pythonOutputDirectory = pythonOutputDirectory;
        this.customOutputDirectory = customOutputDirectory;
        this.descriptorSetFile = descriptorSetFile;
        this.includeImportsInDescriptorSet = includeImportsInDescriptorSet;
        this.includeSourceInfoInDescriptorSet = includeSourceInfoInDescriptorSet;
        this.plugins = plugins;
        this.pluginDirectory = pluginDirectory;
        this.nativePluginId = nativePluginId;
        this.nativePluginExecutable = nativePluginExecutable;
        this.nativePluginParameter = nativePluginParameter;
        this.error = new StringStreamConsumer();
        this.output = new StringStreamConsumer();
    }

    /**
     * Invokes the {@code protoc} compiler using the configuration specified at construction.
     *
     * @return The exit status of {@code protoc}.
     * @throws CommandLineException if command line environment cannot be set up.
     */
    public int execute() throws CommandLineException {
        final Commandline cl = new Commandline();
        cl.setExecutable(executable);
        cl.addArguments(buildProtocCommand().toArray(new String[] {}));
        return CommandLineUtils.executeCommandLine(cl, null, output, error);
    }

    /**
     * Creates the command line arguments.
     *
     * <p>This method has been made visible for testing only.</p>
     *
     * @return A list consisting of the executable followed by any arguments.
     */
    public ImmutableList<String> buildProtocCommand() {
        final List<String> command = newLinkedList();
        // add the executable
        for (final File protoPathElement : protoPathElements) {
            command.add("--proto_path=" + protoPathElement);
        }
        if (javaOutputDirectory != null) {
            command.add("--java_out=" + javaOutputDirectory);

            // For now we assume all custom plugins produce Java output
            for (final ProtocPlugin plugin : plugins) {
                final File pluginExecutable = plugin.getPluginExecutableFile(pluginDirectory);
                command.add("--plugin=protoc-gen-" + plugin.getId() + '=' + pluginExecutable);
                command.add("--" + plugin.getId() + "_out=" + javaOutputDirectory);
            }
        }
        if (javaNanoOutputDirectory != null) {
            String outputOption = "--javanano_out=";
            if (nativePluginParameter != null) {
                outputOption += nativePluginParameter + ':';
            }
            outputOption += javaNanoOutputDirectory;
            command.add(outputOption);
        }
        if (cppOutputDirectory != null) {
            command.add("--cpp_out=" + cppOutputDirectory);
        }
        if (pythonOutputDirectory != null) {
            command.add("--python_out=" + pythonOutputDirectory);
        }
        if (customOutputDirectory != null) {
            if (nativePluginExecutable != null) {
                command.add("--plugin=protoc-gen-" + nativePluginId + '=' + nativePluginExecutable);
            }

            String outputOption = "--" + nativePluginId + "_out=";
            if (nativePluginParameter != null) {
                outputOption += nativePluginParameter + ':';
            }
            outputOption += customOutputDirectory;
            command.add(outputOption);
        }
        for (final File protoFile : protoFiles) {
            command.add(protoFile.toString());
        }
        if (descriptorSetFile != null) {
            command.add("--descriptor_set_out=" + descriptorSetFile);
            if (includeImportsInDescriptorSet) {
                command.add("--include_imports");
            }
            if (includeSourceInfoInDescriptorSet) {
                command.add("--include_source_info");
            }
        }
        return ImmutableList.copyOf(command);
    }

    /**
     * Logs execution parameters on debug level to the specified logger.
     * All log messages will be prefixed with "{@value #LOG_PREFIX}".
     *
     * @param log a logger.
     */
    public void logExecutionParameters(final Log log) {
        if (log.isDebugEnabled()) {
            log.debug(LOG_PREFIX + "Executable: ");
            log.debug(LOG_PREFIX + ' ' + executable);

            if (protoPathElements != null && !protoPathElements.isEmpty()) {
                log.debug(LOG_PREFIX + "Protobuf import paths:");
                for (final File protoPathElement : protoPathElements) {
                    log.debug(LOG_PREFIX + ' ' + protoPathElement);
                }
            }

            if (javaOutputDirectory != null) {
                log.debug(LOG_PREFIX + "Java output directory:");
                log.debug(LOG_PREFIX + ' ' + javaOutputDirectory);

                if (plugins.size() > 0) {
                    log.debug(LOG_PREFIX + "Plugins for Java output:");
                    for (final ProtocPlugin plugin : plugins) {
                        log.debug(LOG_PREFIX + ' ' + plugin.getId());
                    }
                }
            }

            if (pluginDirectory != null) {
                log.debug(LOG_PREFIX + "Plugin directory:");
                log.debug(LOG_PREFIX + ' ' + pluginDirectory);
            }

            if (javaNanoOutputDirectory != null) {
                log.debug(LOG_PREFIX + "Java Nano output directory:");
                log.debug(LOG_PREFIX + ' ' + javaNanoOutputDirectory);
            }
            if (cppOutputDirectory != null) {
                log.debug(LOG_PREFIX + "C++ output directory:");
                log.debug(LOG_PREFIX + ' ' + cppOutputDirectory);
            }
            if (pythonOutputDirectory != null) {
                log.debug(LOG_PREFIX + "Python output directory:");
                log.debug(LOG_PREFIX + ' ' + pythonOutputDirectory);
            }

            if (descriptorSetFile != null) {
                log.debug(LOG_PREFIX + "Descriptor set output file:");
                log.debug(LOG_PREFIX + ' ' + descriptorSetFile);
                log.debug(LOG_PREFIX + "Include imports:");
                log.debug(LOG_PREFIX + ' ' + includeImportsInDescriptorSet);
            }

            log.debug(LOG_PREFIX + "Protobuf descriptors:");
            for (final File protoFile : protoFiles) {
                log.debug(LOG_PREFIX + ' ' + protoFile);
            }

            final List<String> cl = buildProtocCommand();
            if (cl != null && !cl.isEmpty()) {
                log.debug(LOG_PREFIX + "Command line options:");
                log.debug(LOG_PREFIX + Joiner.on(' ').join(cl));
            }
        }
    }

    /**
     * @return the output
     */
    public String getOutput() {
        return output.getOutput();
    }

    /**
     * @return the error
     */
    public String getError() {
        return error.getOutput();
    }

    /**
     * This class builds {@link Protoc} instances.
     */
    static final class Builder {

        /**
         * Path to the {@code protoc} executable.
         */
        private final String executable;

        private final Set<File> protopathElements;

        private final Set<File> protoFiles;

        private final Set<ProtocPlugin> plugins;

        private File pluginDirectory;

        // TODO reorganise support for custom plugins
        // This place is currently a mess because of the two different type of custom plugins supported:
        // pure java (wrapped in a native launcher) and binary native.

        private String nativePluginId;

        private String nativePluginExecutable;

        private String nativePluginParameter;

        /**
         * A directory into which Java source files will be generated.
         */
        private File javaOutputDirectory;

        /**
         * A directory into which Java Nano source files will be generated.
         */
        private File javaNanoOutputDirectory;

        /**
         * A directory into which C++ source files will be generated.
         */
        private File cppOutputDirectory;

        /**
         * A directory into which Python source files will be generated.
         */
        private File pythonOutputDirectory;

        /**
         * A directory into which a custom protoc plugin will generate files.
         */
        private File customOutputDirectory;

        private File descriptorSetFile;

        private boolean includeImportsInDescriptorSet;

        private boolean includeSourceInfoInDescriptorSet;

        /**
         * Constructs a new builder.
         *
         * @param executable The path to the {@code protoc} executable.
         * @throws NullPointerException if {@code executable} is {@code null}.
         */
        Builder(final String executable) {
            this.executable = checkNotNull(executable, "executable");
            this.protoFiles = new LinkedHashSet<File>();
            this.protopathElements = new LinkedHashSet<File>();
            this.plugins = new LinkedHashSet<ProtocPlugin>();
        }

        /**
         * Sets the directory into which Java source files will be generated.
         *
         * @param javaOutputDirectory a directory into which Java source files will be generated.
         * @return this builder instance.
         * @throws NullPointerException if {@code javaOutputDirectory} is {@code null}.
         * @throws IllegalArgumentException if {@code javaOutputDirectory} is not a directory.
         */
        public Builder setJavaOutputDirectory(final File javaOutputDirectory) {
            this.javaOutputDirectory = checkNotNull(javaOutputDirectory, "'javaOutputDirectory' is null");
            checkArgument(javaOutputDirectory.isDirectory(),
                    "'javaOutputDirectory' is not a directory: " + javaOutputDirectory);
            return this;
        }

        /**
         * Sets the directory into which JavaNano source files will be generated.
         *
         * @param javaNanoOutputDirectory a directory into which Java source files will be generated.
         * @return this builder instance.
         * @throws NullPointerException if {@code javaNanoOutputDirectory} is {@code null}.
         * @throws IllegalArgumentException if {@code javaNanoOutputDirectory} is not a directory.
         */
        public Builder setJavaNanoOutputDirectory(final File javaNanoOutputDirectory) {
            this.javaNanoOutputDirectory = checkNotNull(javaNanoOutputDirectory,
                    "'javaNanoOutputDirectory' is null");
            checkArgument(javaNanoOutputDirectory.isDirectory(),
                    "'javaNanoOutputDirectory' is not a directory: " + javaNanoOutputDirectory);
            return this;
        }

        /**
         * Sets the directory into which C++ source files will be generated.
         *
         * @param cppOutputDirectory a directory into which C++ source files will be generated.
         * @return this builder instance.
         * @throws NullPointerException if {@code cppOutputDirectory} is {@code null}.
         * @throws IllegalArgumentException if {@code cppOutputDirectory} is not a directory.
         */
        public Builder setCppOutputDirectory(final File cppOutputDirectory) {
            this.cppOutputDirectory = checkNotNull(cppOutputDirectory, "'cppOutputDirectory' is null");
            checkArgument(cppOutputDirectory.isDirectory(),
                    "'cppOutputDirectory' is not a directory: " + cppOutputDirectory);
            return this;
        }

        /**
         * Sets the directory into which Python source files will be generated.
         *
         * @param pythonOutputDirectory a directory into which Python source files will be generated.
         * @return this builder instance.
         * @throws NullPointerException if {@code pythonOutputDirectory} is {@code null}.
         * @throws IllegalArgumentException if {@code pythonOutputDirectory} is not a directory.
         */
        public Builder setPythonOutputDirectory(final File pythonOutputDirectory) {
            this.pythonOutputDirectory = checkNotNull(pythonOutputDirectory, "'pythonOutputDirectory' is null");
            checkArgument(pythonOutputDirectory.isDirectory(),
                    "'pythonOutputDirectory' is not a directory: " + pythonOutputDirectory);
            return this;
        }

        /**
         * Sets the directory into which a custom protoc plugin will generate files.
         *
         * @param customOutputDirectory a directory into which a custom protoc plugin will generate files.
         * @return this builder instance.
         * @throws NullPointerException if {@code customOutputDirectory} is {@code null}.
         * @throws IllegalArgumentException if {@code customOutputDirectory} is not a directory.
         */
        public Builder setCustomOutputDirectory(final File customOutputDirectory) {
            this.customOutputDirectory = checkNotNull(customOutputDirectory, "'customOutputDirectory' is null");
            checkArgument(customOutputDirectory.isDirectory(),
                    "'customOutputDirectory' is not a directory: " + customOutputDirectory);
            return this;
        }

        /**
         * Adds a proto file to be compiled. Proto files must be on the protopath
         * and this method will fail if a proto file is added without first adding a
         * parent directory to the protopath.
         *
         * @param protoFile source protobuf definitions file.
         * @return The builder.
         * @throws IllegalStateException If a proto file is added without first
         * adding a parent directory to the protopath.
         * @throws NullPointerException If {@code protoFile} is {@code null}.
         */
        public Builder addProtoFile(final File protoFile) {
            checkNotNull(protoFile);
            checkArgument(protoFile.isFile());
            checkArgument(protoFile.getName().endsWith(".proto"));
            checkProtoFileIsInProtopath(protoFile);
            protoFiles.add(protoFile);
            return this;
        }

        /**
         * Adds a protoc plugin definition for custom code generation.
         * @param plugin plugin definition
         * @return this builder instance.
         */
        public Builder addPlugin(final ProtocPlugin plugin) {
            checkNotNull(plugin);
            plugins.add(plugin);
            return this;
        }

        public Builder setPluginDirectory(final File directory) {
            checkNotNull(directory);
            checkArgument(directory.isDirectory(), "Plugin directory " + directory + "does not exist");
            pluginDirectory = directory;
            return this;
        }

        public void setNativePluginId(final String nativePluginId) {
            checkNotNull(nativePluginId, "'nativePluginId' is null");
            checkArgument(!nativePluginId.isEmpty(), "'nativePluginId' is empty");
            checkArgument(
                    !(nativePluginId.equals("java") || nativePluginId.equals("javanano")
                            || nativePluginId.equals("python") || nativePluginId.equals("cpp")
                            || nativePluginId.equals("descriptor_set")),
                    "'nativePluginId' matches one of the built-in protoc plugins");
            this.nativePluginId = nativePluginId;
        }

        public void setNativePluginExecutable(final String nativePluginExecutable) {
            checkNotNull(nativePluginExecutable, "'nativePluginExecutable' is null");
            this.nativePluginExecutable = nativePluginExecutable;
        }

        public void setNativePluginParameter(final String nativePluginParameter) {
            checkNotNull(nativePluginParameter, "'nativePluginParameter' is null");
            checkArgument(!nativePluginParameter.contains(":"),
                    "'nativePluginParameter' contains illegal characters");
            this.nativePluginParameter = nativePluginParameter;
        }

        public Builder withDescriptorSetFile(final File descriptorSetFile, final boolean includeImports,
                final boolean includeSourceInfoInDescriptorSet) {
            checkNotNull(descriptorSetFile, "descriptorSetFile");
            checkArgument(descriptorSetFile.getParentFile().isDirectory());
            this.descriptorSetFile = descriptorSetFile;
            this.includeImportsInDescriptorSet = includeImports;
            this.includeSourceInfoInDescriptorSet = includeSourceInfoInDescriptorSet;
            return this;
        }

        private void checkProtoFileIsInProtopath(final File protoFile) {
            assert protoFile.isFile();
            checkState(checkProtoFileIsInProtopathHelper(protoFile.getParentFile()));
        }

        private boolean checkProtoFileIsInProtopathHelper(final File directory) {
            assert directory.isDirectory();
            if (protopathElements.contains(directory)) {
                return true;
            } else {
                final File parentDirectory = directory.getParentFile();
                return parentDirectory != null && checkProtoFileIsInProtopathHelper(parentDirectory);
            }
        }

        /**
         * Adds a collection of proto files to be compiled.
         *
         * @param protoFiles a collection of source protobuf definition files.
         * @return this builder instance.
         * @see #addProtoFile(File)
         */
        public Builder addProtoFiles(final Iterable<File> protoFiles) {
            for (final File protoFile : protoFiles) {
                addProtoFile(protoFile);
            }
            return this;
        }

        /**
         * Adds the {@code protopathElement} to the protopath.
         *
         * @param protopathElement A directory to be searched for imported protocol buffer definitions.
         * @return The builder.
         * @throws NullPointerException If {@code protopathElement} is {@code null}.
         * @throws IllegalArgumentException If {@code protpathElement} is not a
         * directory.
         */
        public Builder addProtoPathElement(final File protopathElement) {
            checkNotNull(protopathElement);
            checkArgument(protopathElement.isDirectory());
            protopathElements.add(protopathElement);
            return this;
        }

        /**
         * Adds a number of elements to the protopath.
         *
         * @param protopathElements directories to be searched for imported protocol buffer definitions.
         * @return this builder instance.
         * @see #addProtoPathElement(File)
         */
        public Builder addProtoPathElements(final Iterable<File> protopathElements) {
            for (final File protopathElement : protopathElements) {
                addProtoPathElement(protopathElement);
            }
            return this;
        }

        /**
         * Validates the internal state for consistency and completeness.
         */
        private void validateState() {
            checkState(!protoFiles.isEmpty());
            checkState(
                    javaOutputDirectory != null || javaNanoOutputDirectory != null || cppOutputDirectory != null
                            || pythonOutputDirectory != null || customOutputDirectory != null,
                    "At least one of these properties must be set: "
                            + "'javaOutputDirectory', 'javaNanoOutputDirectory', 'cppOutputDirectory', "
                            + "'pythonOutputDirectory' or 'customOutputDirectory'");
        }

        /**
         * Builds and returns a fully configured instance of {@link Protoc} wrapper.
         *
         * @return a configured {@link Protoc} instance.
         * @throws IllegalStateException if builder state is incomplete or inconsistent.
         */
        public Protoc build() {
            validateState();
            return new Protoc(executable, ImmutableSet.copyOf(protopathElements), ImmutableSet.copyOf(protoFiles),
                    javaOutputDirectory, javaNanoOutputDirectory, cppOutputDirectory, pythonOutputDirectory,
                    customOutputDirectory, descriptorSetFile, includeImportsInDescriptorSet,
                    includeSourceInfoInDescriptorSet, ImmutableSet.copyOf(plugins), pluginDirectory, nativePluginId,
                    nativePluginExecutable, nativePluginParameter);
        }
    }
}