Plmerge receives arguments by a protobuf, introduces variable substitutions to plmerge.

As of this change plmerge can consume either a protobuf or command line arguments.  Once bazel uses plmerge strictly with protobufs, the command line arguments will be deprecated.

--
MOS_MIGRATED_REVID=109716003
diff --git a/src/main/protobuf/BUILD b/src/main/protobuf/BUILD
index 7bff28a..c192a0d 100644
--- a/src/main/protobuf/BUILD
+++ b/src/main/protobuf/BUILD
@@ -12,6 +12,7 @@
     "android_studio_ide_info",
     "package_manifest",
     "test_status",
+    "plmerge",
     "bundlemerge",
     "xcodegen",
     "worker_protocol",
diff --git a/src/main/protobuf/plmerge.proto b/src/main/protobuf/plmerge.proto
new file mode 100644
index 0000000..d493a6f
--- /dev/null
+++ b/src/main/protobuf/plmerge.proto
@@ -0,0 +1,48 @@
+// Copyright 2015 The Bazel 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.
+syntax = "proto2";
+
+package devtools.xcode;
+option java_outer_classname = "PlMergeProtos";
+option java_package = "com.google.devtools.build.xcode.plmerge.proto";
+
+// Contains necessary arguments for PlMerge, which is responsible for merging
+// plist files.
+message Control {
+  // Paths to the plist files to merge relative to execution root.
+  repeated string source_file = 1;
+
+  // Path to the output file to merge relative to execution root.
+  required string out_file = 2;
+
+  // Key-value substitutions to support templating for plists.  A substitution
+  // is made if the substitution key appears as a value for any key-value pair
+  // in any source_file.
+  // For example, a plist with the entry:
+  //    <key>CFBundleExectuable</key>
+  //    <string>EXECUTABLE_NAME</string>
+  // could be templated by passing a variable substitution like
+  //    {"EXECUTABLE_NAME", "PrenotCalculator"}
+  map<string, string> variable_substitution_map = 3;
+
+  // A reverse-DNS string identifier for this bundle associated with output
+  // binary plist.  Overrides the bundle id specified in the CFBundleIdentifier
+  // plist field.
+  optional string primary_bundle_id = 4;
+
+  // A fallback reverse-DNS string identifier for this bundle when bundle
+  // identifier is not specified in primary_bundle_id or an associated plist
+  // file.
+  optional string fallback_bundle_id = 5;
+}
\ No newline at end of file
diff --git a/src/objc_tools/plmerge/BUILD b/src/objc_tools/plmerge/BUILD
index 2dd46af..81268bc 100644
--- a/src/objc_tools/plmerge/BUILD
+++ b/src/objc_tools/plmerge/BUILD
@@ -8,6 +8,7 @@
     deps = [
         ":plmerge_lib",
         "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:plmerge_proto",
         "//third_party:guava",
         "//third_party/java/dd_plist",
     ],
@@ -17,9 +18,10 @@
     name = "plmerge_lib",
     srcs = glob(
         ["java/**/*.java"],
-        exclude = ["java/**/PlMerge.java"],
     ),
     deps = [
+        "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:plmerge_proto",
         "//src/tools/xcode-common/java/com/google/devtools/build/xcode/common",
         "//src/tools/xcode-common/java/com/google/devtools/build/xcode/util",
         "//third_party:guava",
diff --git a/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/MergingArguments.java b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/MergingArguments.java
new file mode 100644
index 0000000..d278dc8
--- /dev/null
+++ b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/MergingArguments.java
@@ -0,0 +1,117 @@
+// Copyright 2015 The Bazel 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.
+package com.google.devtools.build.xcode.plmerge;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.xcode.plmerge.PlMerge.PlMergeOptions;
+import com.google.devtools.build.xcode.plmerge.proto.PlMergeProtos.Control;
+
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Container for data consumed by plmerge
+ */
+class MergingArguments {
+
+  private final FileSystem fileSystem = FileSystems.getDefault();
+  private final List<Path> sourceFilePaths;
+  private final String outFile;
+  private final Map<String, String> variableSubstitutions;
+  private final String primaryBundleId;
+  private final String fallbackBundleId;
+
+  /**
+   * Build MergingArguments from a plmerge protobuf.
+   */
+  public MergingArguments(Control control) {
+    ImmutableList.Builder<Path> sourceFilePathsBuilder = new Builder<>();
+    for (String pathString : control.getSourceFileList()) {
+      sourceFilePathsBuilder.add(fileSystem.getPath(pathString));
+    }
+    sourceFilePaths = sourceFilePathsBuilder.build();
+    outFile = control.getOutFile();
+    variableSubstitutions = control.getVariableSubstitutionMap();
+    primaryBundleId = control.getPrimaryBundleId();
+    fallbackBundleId = control.getFallbackBundleId();
+  }
+
+  /**
+   * Build MergingArguments from command line arguments passed to the plmerge executable.
+   */
+  public MergingArguments(PlMergeOptions options) {
+    ImmutableList.Builder<Path> sourceFilePathsBuilder = new Builder<Path>();
+    for (String sourceFile : options.sourceFiles) {
+      sourceFilePathsBuilder.add(fileSystem.getPath(sourceFile));
+    }
+
+    sourceFilePaths = sourceFilePathsBuilder.build();
+    outFile = options.outFile;
+    variableSubstitutions = ImmutableMap.<String, String>of();
+    primaryBundleId = options.primaryBundleId;
+    fallbackBundleId = options.fallbackBundleId;
+  }
+
+  /**
+   * Returns paths to the plist files to merge relative to plmerge. These can be
+   * binary, XML, or ASCII format.
+   */
+  public List<Path> getSourceFilePaths() {
+    return sourceFilePaths;
+  }
+
+  /**
+   * Returns path to the output file to merge relative to plmerge.
+   */
+  public String getOutFile() {
+    return outFile;
+  }
+
+  /**
+   * Returns a reverse-DNS string identifier for this bundle associated with output
+   * binary plist.  Overrides the bundle id specified in the CFBundleIdentifier
+   * plist field.
+   */
+  public String getPrimaryBundleId() {
+    return primaryBundleId;
+  }
+
+  /**
+   * Returns a fallback reverse-DNS string identifier for this bundle when bundle
+   * identifier is not specified in primary_bundle_id or an associated plist
+   * file.
+   */
+  public String getFallbackBundleId() {
+    return fallbackBundleId;
+  }
+
+  /**
+   * Returns key-value substitutions to support templating for plists.  A substitution
+   * is made if the substitution key appears as a value for any key-value pair
+   * in any source_file.
+   * For example, a plist with the entry:
+   *    <pre><key>CFBundleExectuable</key>
+   *    <string>EXECUTABLE_NAME</string></pre>
+   * could be templated by passing a variable substitution like
+   *    {"EXECUTABLE_NAME", "PrenotCalculator"}
+   */
+  public Map<String, String> getVariableSubstitutions() {
+    return variableSubstitutions;
+  }
+}
diff --git a/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlMerge.java b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlMerge.java
index ea13c29..40eaa57 100644
--- a/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlMerge.java
+++ b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlMerge.java
@@ -15,6 +15,7 @@
 package com.google.devtools.build.xcode.plmerge;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.xcode.plmerge.proto.PlMergeProtos.Control;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.Options;
 import com.google.devtools.common.options.OptionsBase;
@@ -24,81 +25,123 @@
 import com.dd.plist.NSObject;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
-import java.nio.file.Path;
-import java.util.ArrayList;
+import java.nio.file.Files;
 import java.util.List;
 
 /**
  * Entry point for the {@code plmerge} tool, which merges the data from one or more plists into a
  * single binary plist. This tool's functionality is similar to that of the
  * {@code builtin-infoPlistUtility} in Xcode.
+ * 
+ * <p>For backwards compatibility, PlMerge can consume either a control protobuf, passed using 
+ * --control, or the command line arguments --source_file, --out_file, --primary_bundle_id,
+ * and --fallback_bundle_id.  If a --control is not provided, PlMerge will fall back on the other
+ * command line arguments.  If --control is provided, all other command line arguments are ignored.
  */
 public class PlMerge {
+
   /**
    * Options for {@link PlMerge}.
    */
   public static class PlMergeOptions extends OptionsBase {
+        
     @Option(
-        name = "source_file",
-        help = "Paths to the plist files to merge. These can be binary, XML, or ASCII format. "
-            + "Repeat this flag to specify multiple files. Required.",
-        allowMultiple = true,
-        defaultValue = "null")
+      name = "source_file",
+      help =
+          "Paths to the plist files to merge. These can be binary, XML, or ASCII format. "
+              + "Repeat this flag to specify multiple files. Required.",
+      allowMultiple = true,
+      defaultValue = "null"
+    )
     public List<String> sourceFiles;
 
-    @Option(
-        name = "out_file",
-        help = "Path to the output file. Required.",
-        defaultValue = "null")
+    @Option(name = "out_file", help = "Path to the output file. Required.", defaultValue = "null")
     public String outFile;
 
     @Option(
-        name = "primary_bundle_id",
-        help = "A reverse-DNS string identifier for this bundle associated with output binary "
-            + "plist. This flag overrides the bundle id specified in field CFBundleIdentifier in "
-            + "the associated plist file.",
-        defaultValue = "null")
+      name = "primary_bundle_id",
+      help =
+          "A reverse-DNS string identifier for this bundle associated with output binary "
+              + "plist. This flag overrides the bundle id specified in field CFBundleIdentifier in "
+              + "the associated plist file.",
+      defaultValue = "null"
+    )
     public String primaryBundleId;
 
     @Option(
-        name = "fallback_bundle_id",
-        help = "A fallback reverse-DNS string identifier for this bundle when the bundle "
-            + "identifier is not specified in flag primary_bundle_id or associated plist file",
-        defaultValue = "null")
+      name = "fallback_bundle_id",
+      help =
+          "A fallback reverse-DNS string identifier for this bundle when the bundle "
+              + "identifier is not specified in flag primary_bundle_id or associated plist file",
+      defaultValue = "null"
+    )
     public String fallbackBundleId;
-  }
 
-  public static void main(String[] args) throws IOException, OptionsParsingException {
+    @Option(
+      name = "control",
+      help =
+          "Absolute path of the Control protobuf. Data can be passed to plmerge through this "
+              + "protobuf or through source_file, out_file, primary_bundle_id and "
+              + "fallback_bundle_id.",
+      defaultValue = "null"
+    )
+    public String controlPath;
+  }
+  
+  public static void main(String[] args) throws OptionsParsingException, IOException {
+
+    FileSystem fileSystem = FileSystems.getDefault();
     OptionsParser parser = OptionsParser.newOptionsParser(PlMergeOptions.class);
     parser.parse(args);
     PlMergeOptions options = parser.getOptions(PlMergeOptions.class);
-    if (options.sourceFiles.isEmpty()) {
-      missingArg("At least one --source_file");
-    }
-    if (options.outFile == null) {
-      missingArg("--out_file");
-    }
-    FileSystem fileSystem = FileSystems.getDefault();
 
-    List<Path> sourceFilePaths = new ArrayList<>();
-    for (String sourceFile : options.sourceFiles) {
-      sourceFilePaths.add(fileSystem.getPath(sourceFile));
+    MergingArguments data = null;
+
+    if (usingControlProtobuf(options)) {
+      InputStream in = Files.newInputStream(fileSystem.getPath(options.controlPath));
+      Control control = Control.parseFrom(in);
+      validateControl(control);
+      data = new MergingArguments(control);
+    } else if (usingCommandLineArgs(options)) {
+      data = new MergingArguments(options);
+    } else {
+      missingArg("Either --control or --out_file and at least one --source_file");
     }
 
-    PlistMerging merging = PlistMerging.from(sourceFilePaths, ImmutableMap.<String, NSObject>of(),
-        ImmutableMap.<String, String>of(), new KeysToRemoveIfEmptyString());
-    if (options.primaryBundleId != null || options.fallbackBundleId != null) {
+    PlistMerging merging =
+        PlistMerging.from(
+            data,
+            ImmutableMap.<String, NSObject>of(),
+            new KeysToRemoveIfEmptyString("CFBundleIconFile", "NSPrincipalClass"));
+    if (data.getPrimaryBundleId() != null || data.getFallbackBundleId() != null) {
       // Only set the bundle identifier if we were passed arguments to do so.
       // This prevents CFBundleIdentifiers being put into strings files.
-      merging.setBundleIdentifier(options.primaryBundleId, options.fallbackBundleId);
+      merging.setBundleIdentifier(data.getPrimaryBundleId(), data.getFallbackBundleId());
     }
-    merging.writePlist(fileSystem.getPath(options.outFile));
+    merging.writePlist(fileSystem.getPath(data.getOutFile()));
   }
 
+  private static void validateControl(Control control) {
+    if (control.getSourceFileList().isEmpty()) {
+      missingArg("At least one source_file");
+    } else if (!control.hasOutFile()) {
+      missingArg("out_file");
+    }
+  }
+  
   private static void missingArg(String flag) {
     throw new IllegalArgumentException(flag + " is required:\n"
         + Options.getUsage(PlMergeOptions.class));
   }
+  
+  private static boolean usingControlProtobuf(PlMergeOptions options) {
+    return options.controlPath != null;
+  }
+
+  private static boolean usingCommandLineArgs(PlMergeOptions options) {
+    return (!options.sourceFiles.isEmpty()) && (options.outFile != null);
+  }
 }
diff --git a/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlistMerging.java b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlistMerging.java
index 31201ee..4dae6fc 100644
--- a/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlistMerging.java
+++ b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlistMerging.java
@@ -176,6 +176,22 @@
    * Generates a Plistmerging combining values from sourceFiles and automaticEntries, and modifying
    * them based on subsitutions and keysToRemoveIfEmptyString.
    */
+  public static PlistMerging from(
+      MergingArguments mergingArguments,
+      Map<String, NSObject> automaticEntries,
+      KeysToRemoveIfEmptyString keysToRemoveIfEmptyString)
+      throws IOException {
+    return from(
+        mergingArguments.getSourceFilePaths(),
+        automaticEntries,
+        mergingArguments.getVariableSubstitutions(),
+        keysToRemoveIfEmptyString);
+  }
+  
+  /**
+   * Generates a Plistmerging combining values from sourceFiles and automaticEntries, and modifying
+   * them based on subsitutions and keysToRemoveIfEmptyString.
+   */
   public static PlistMerging from(List<Path> sourceFiles, Map<String, NSObject> automaticEntries,
       Map<String, String> substitutions, KeysToRemoveIfEmptyString keysToRemoveIfEmptyString)
           throws IOException {