changes to allow for generation of alternate format metadata
authordbeaumont@google.com <dbeaumont@google.com@ee073f10-1060-11df-b6a4-87a95322a99c>
Fri, 20 Jul 2012 14:21:29 +0000 (14:21 +0000)
committerdbeaumont@google.com <dbeaumont@google.com@ee073f10-1060-11df-b6a4-87a95322a99c>
Fri, 20 Jul 2012 14:21:29 +0000 (14:21 +0000)
git-svn-id: http://libphonenumber.googlecode.com/svn/trunk@507 ee073f10-1060-11df-b6a4-87a95322a99c

tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java
tools/java/common/test/com/google/i18n/phonenumbers/BuildMetadataFromXmlTest.java
tools/java/cpp-build/src/com/google/i18n/phonenumbers/BuildMetadataCppFromXml.java
tools/java/cpp-build/src/com/google/i18n/phonenumbers/CppMetadataGenerator.java [new file with mode: 0644]
tools/java/cpp-build/target/cpp-build-1.0-SNAPSHOT-jar-with-dependencies.jar
tools/java/cpp-build/test/com/google/i18n/phonenumbers/BuildMetadataCppFromXmlTest.java
tools/java/cpp-build/test/com/google/i18n/phonenumbers/CppMetadataGeneratorTest.java [new file with mode: 0644]
tools/java/java-build/target/java-build-1.0-SNAPSHOT-jar-with-dependencies.jar

index a5fb1ca..67298ac 100644 (file)
@@ -44,7 +44,6 @@ import javax.xml.parsers.DocumentBuilderFactory;
  */
 public class BuildMetadataFromXml {
   private static final Logger LOGGER = Logger.getLogger(BuildMetadataFromXml.class.getName());
-  private static boolean liteBuild;
 
   // String constants used to fetch the XML nodes and attributes.
   private static final String CARRIER_CODE_FORMATTING_RULE = "carrierCodeFormattingRule";
@@ -82,15 +81,9 @@ public class BuildMetadataFromXml {
   private static final String VOICEMAIL = "voicemail";
   private static final String VOIP = "voip";
 
-  // @VisibleForTesting
-  static void setLiteBuild(boolean b) {
-    liteBuild = b;
-  }
-
   // Build the PhoneMetadataCollection from the input XML file.
   public static PhoneMetadataCollection buildPhoneMetadataCollection(String inputXmlFile,
       boolean liteBuild) throws Exception {
-    BuildMetadataFromXml.liteBuild = liteBuild;
     DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
     DocumentBuilder builder = builderFactory.newDocumentBuilder();
     File xmlFile = new File(inputXmlFile);
@@ -108,7 +101,7 @@ public class BuildMetadataFromXml {
       if (territoryElement.hasAttribute("id")) {
         regionCode = territoryElement.getAttribute("id");
       }
-      PhoneMetadata metadata = loadCountryMetadata(regionCode, territoryElement);
+      PhoneMetadata metadata = loadCountryMetadata(regionCode, territoryElement, liteBuild);
       metadataCollection.addMetadata(metadata);
     }
     return metadataCollection.build();
@@ -389,7 +382,8 @@ public class BuildMetadataFromXml {
   // @VisibleForTesting
   static PhoneNumberDesc.Builder processPhoneNumberDescElement(PhoneNumberDesc.Builder generalDesc,
                                                                Element countryElement,
-                                                               String numberType) {
+                                                               String numberType,
+                                                               boolean liteBuild) {
     NodeList phoneNumberDescList = countryElement.getElementsByTagName(numberType);
     PhoneNumberDesc.Builder numberDesc = PhoneNumberDesc.newBuilder();
     if (phoneNumberDescList.getLength() == 0 && !isValidNumberType(numberType)) {
@@ -423,31 +417,42 @@ public class BuildMetadataFromXml {
   }
 
   // @VisibleForTesting
-  static void loadGeneralDesc(PhoneMetadata.Builder metadata, Element element) {
+  static void loadGeneralDesc(PhoneMetadata.Builder metadata, Element element, boolean liteBuild) {
     PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
-    generalDesc = processPhoneNumberDescElement(generalDesc, element, GENERAL_DESC);
+    generalDesc = processPhoneNumberDescElement(generalDesc, element, GENERAL_DESC, liteBuild);
     metadata.setGeneralDesc(generalDesc);
 
-    metadata.setFixedLine(processPhoneNumberDescElement(generalDesc, element, FIXED_LINE));
-    metadata.setMobile(processPhoneNumberDescElement(generalDesc, element, MOBILE));
-    metadata.setTollFree(processPhoneNumberDescElement(generalDesc, element, TOLL_FREE));
-    metadata.setPremiumRate(processPhoneNumberDescElement(generalDesc, element, PREMIUM_RATE));
-    metadata.setSharedCost(processPhoneNumberDescElement(generalDesc, element, SHARED_COST));
-    metadata.setVoip(processPhoneNumberDescElement(generalDesc, element, VOIP));
-    metadata.setPersonalNumber(processPhoneNumberDescElement(generalDesc, element,
-                                                             PERSONAL_NUMBER));
-    metadata.setPager(processPhoneNumberDescElement(generalDesc, element, PAGER));
-    metadata.setUan(processPhoneNumberDescElement(generalDesc, element, UAN));
-    metadata.setVoicemail(processPhoneNumberDescElement(generalDesc, element, VOICEMAIL));
-    metadata.setEmergency(processPhoneNumberDescElement(generalDesc, element, EMERGENCY));
-    metadata.setNoInternationalDialling(processPhoneNumberDescElement(generalDesc, element,
-                                                                      NO_INTERNATIONAL_DIALLING));
+    metadata.setFixedLine(
+        processPhoneNumberDescElement(generalDesc, element, FIXED_LINE, liteBuild));
+    metadata.setMobile(
+        processPhoneNumberDescElement(generalDesc, element, MOBILE, liteBuild));
+    metadata.setTollFree(
+        processPhoneNumberDescElement(generalDesc, element, TOLL_FREE, liteBuild));
+    metadata.setPremiumRate(
+        processPhoneNumberDescElement(generalDesc, element, PREMIUM_RATE, liteBuild));
+    metadata.setSharedCost(
+        processPhoneNumberDescElement(generalDesc, element, SHARED_COST, liteBuild));
+    metadata.setVoip(
+        processPhoneNumberDescElement(generalDesc, element, VOIP, liteBuild));
+    metadata.setPersonalNumber(
+        processPhoneNumberDescElement(generalDesc, element, PERSONAL_NUMBER, liteBuild));
+    metadata.setPager(
+        processPhoneNumberDescElement(generalDesc, element, PAGER, liteBuild));
+    metadata.setUan(
+        processPhoneNumberDescElement(generalDesc, element, UAN, liteBuild));
+    metadata.setVoicemail(
+        processPhoneNumberDescElement(generalDesc, element, VOICEMAIL, liteBuild));
+    metadata.setEmergency(
+        processPhoneNumberDescElement(generalDesc, element, EMERGENCY, liteBuild));
+    metadata.setNoInternationalDialling(
+        processPhoneNumberDescElement(generalDesc, element, NO_INTERNATIONAL_DIALLING, liteBuild));
     metadata.setSameMobileAndFixedLinePattern(
         metadata.getMobile().getNationalNumberPattern().equals(
         metadata.getFixedLine().getNationalNumberPattern()));
   }
 
-  public static PhoneMetadata loadCountryMetadata(String regionCode, Element element) {
+  // @VisibleForTesting
+  static PhoneMetadata loadCountryMetadata(String regionCode, Element element, boolean liteBuild) {
     String nationalPrefix = getNationalPrefix(element);
     PhoneMetadata.Builder metadata =
         loadTerritoryTagMetadata(regionCode, element, nationalPrefix);
@@ -456,7 +461,7 @@ public class BuildMetadataFromXml {
     loadAvailableFormats(metadata, element, nationalPrefix.toString(),
                          nationalPrefixFormattingRule.toString(),
                          element.hasAttribute(NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING));
-    loadGeneralDesc(metadata, element);
+    loadGeneralDesc(metadata, element, liteBuild);
     return metadata.build();
   }
 }
index f46da32..bed22b7 100644 (file)
@@ -390,7 +390,7 @@ public class BuildMetadataFromXmlTest extends TestCase {
     PhoneNumberDesc.Builder phoneNumberDesc;
 
     phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
-        generalDesc, territoryElement, "invalidType");
+        generalDesc, territoryElement, "invalidType", false);
     assertEquals("NA", phoneNumberDesc.getPossibleNumberPattern());
     assertEquals("NA", phoneNumberDesc.getNationalNumberPattern());
   }
@@ -403,7 +403,7 @@ public class BuildMetadataFromXmlTest extends TestCase {
     PhoneNumberDesc.Builder phoneNumberDesc;
 
     phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
-        generalDesc, territoryElement, "fixedLine");
+        generalDesc, territoryElement, "fixedLine", false);
     assertEquals("\\d{6}", phoneNumberDesc.getPossibleNumberPattern());
   }
 
@@ -419,30 +419,23 @@ public class BuildMetadataFromXmlTest extends TestCase {
     PhoneNumberDesc.Builder phoneNumberDesc;
 
     phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
-        generalDesc, territoryElement, "fixedLine");
+        generalDesc, territoryElement, "fixedLine", false);
     assertEquals("\\d{6}", phoneNumberDesc.getPossibleNumberPattern());
   }
 
   public void testProcessPhoneNumberDescElementHandlesLiteBuild()
       throws ParserConfigurationException, SAXException, IOException {
-    try {
-      BuildMetadataFromXml.setLiteBuild(true);
-      PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
-      String xmlInput =
-          "<territory><fixedLine>" +
-          "  <exampleNumber>01 01 01 01</exampleNumber>" +
-          "</fixedLine></territory>";
-      Element territoryElement = parseXmlString(xmlInput);
-      PhoneNumberDesc.Builder phoneNumberDesc;
-
-      phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
-          generalDesc, territoryElement, "fixedLine");
-      assertEquals("", phoneNumberDesc.getExampleNumber());
-    } finally {
-      // Restore the lite build parameter to its default value (false) to avoid potential
-      // side-effects in other tests.
-      BuildMetadataFromXml.setLiteBuild(false);
-    }
+    PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+    String xmlInput =
+        "<territory><fixedLine>" +
+        "  <exampleNumber>01 01 01 01</exampleNumber>" +
+        "</fixedLine></territory>";
+    Element territoryElement = parseXmlString(xmlInput);
+    PhoneNumberDesc.Builder phoneNumberDesc;
+
+    phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
+        generalDesc, territoryElement, "fixedLine", true);
+    assertEquals("", phoneNumberDesc.getExampleNumber());
   }
 
   public void testProcessPhoneNumberDescOutputsExampleNumberByDefault()
@@ -450,13 +443,13 @@ public class BuildMetadataFromXmlTest extends TestCase {
     PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
     String xmlInput =
         "<territory><fixedLine>" +
-         "  <exampleNumber>01 01 01 01</exampleNumber>" +
-         "</fixedLine></territory>";
+        "  <exampleNumber>01 01 01 01</exampleNumber>" +
+        "</fixedLine></territory>";
     Element territoryElement = parseXmlString(xmlInput);
     PhoneNumberDesc.Builder phoneNumberDesc;
 
     phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
-        generalDesc, territoryElement, "fixedLine");
+        generalDesc, territoryElement, "fixedLine", false);
     assertEquals("01 01 01 01", phoneNumberDesc.getExampleNumber());
   }
 
@@ -465,13 +458,13 @@ public class BuildMetadataFromXmlTest extends TestCase {
     PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
     String xmlInput =
         "<territory><fixedLine>" +
-         "  <possibleNumberPattern>\t \\d { 6 } </possibleNumberPattern>" +
-         "</fixedLine></territory>";
+        "  <possibleNumberPattern>\t \\d { 6 } </possibleNumberPattern>" +
+        "</fixedLine></territory>";
     Element countryElement = parseXmlString(xmlInput);
     PhoneNumberDesc.Builder phoneNumberDesc;
 
     phoneNumberDesc = BuildMetadataFromXml.processPhoneNumberDescElement(
-        generalDesc, countryElement, "fixedLine");
+        generalDesc, countryElement, "fixedLine", false);
     assertEquals("\\d{6}", phoneNumberDesc.getPossibleNumberPattern());
   }
 
@@ -486,7 +479,7 @@ public class BuildMetadataFromXmlTest extends TestCase {
     Element territoryElement = parseXmlString(xmlInput);
     PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
     // Should set sameMobileAndFixedPattern to true.
-    BuildMetadataFromXml.loadGeneralDesc(metadata, territoryElement);
+    BuildMetadataFromXml.loadGeneralDesc(metadata, territoryElement, false);
     assertTrue(metadata.isSameMobileAndFixedLinePattern());
   }
 
@@ -504,10 +497,10 @@ public class BuildMetadataFromXmlTest extends TestCase {
         "  <voip><nationalNumberPattern>\\d{8}</nationalNumberPattern></voip>" +
         "  <uan><nationalNumberPattern>\\d{9}</nationalNumberPattern></uan>" +
         "  <shortCode><nationalNumberPattern>\\d{10}</nationalNumberPattern></shortCode>" +
-         "</territory>";
+        "</territory>";
     Element territoryElement = parseXmlString(xmlInput);
     PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
-    BuildMetadataFromXml.loadGeneralDesc(metadata, territoryElement);
+    BuildMetadataFromXml.loadGeneralDesc(metadata, territoryElement, false);
     assertEquals("\\d{1}", metadata.getFixedLine().getNationalNumberPattern());
     assertEquals("\\d{2}", metadata.getMobile().getNationalNumberPattern());
     assertEquals("\\d{3}", metadata.getPager().getNationalNumberPattern());
index ddc01e3..baf3422 100644 (file)
 
 package com.google.i18n.phonenumbers;
 
-import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
+import com.google.i18n.phonenumbers.CppMetadataGenerator.Type;
 
 import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.PrintWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
 import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * This class generates the C++ code representation of the provided XML metadata file. It lets us
@@ -32,35 +36,112 @@ import java.util.Set;
  * the code emitted by this class with the C++ phonenumber library.
  *
  * @author Philippe Liard
+ * @author David Beaumont
  */
 public class BuildMetadataCppFromXml extends Command {
-  // File path where the XML input can be found.
-  private String inputFilePath;
-  // Output directory where the generated files will be saved.
-  private String outputDir;
-  // 'metadata', 'test_metadata' or 'lite_metadata' depending on the value of the last command line
-  // parameter.
-  private String baseFilename;
-  // Whether to generate "lite" metadata or not.
-  private boolean liteMetadata;
-  // The binary translation of the XML file is directly written to a byte array output stream
-  // instead of creating an unnecessary file on the filesystem.
-  private ByteArrayOutputStream binaryStream = new ByteArrayOutputStream();
-
-  // Header (.h) file and implementation (.cc) file output streams.
-  private FileOutputStream headerFileOutputStream;
-  private FileOutputStream implFileOutputStream;
-
-  private static final Set<String> METADATA_TYPES =
-      new HashSet<String>(Arrays.asList("metadata", "test_metadata", "lite_metadata"));
-
-  private static final int COPYRIGHT_YEAR = 2011;
+
+  /** An enum encapsulating the variations of metadata that we can produce. */
+  public enum Variant {
+    /** The default 'full' variant which contains all the metadata. */
+    FULL("%s"),
+    /** The test variant which contains fake data for tests. */
+    TEST("test_%s"),
+    /**
+     * The lite variant contains the same metadata as the full version but excludes any example
+     * data. This is typically used for clients with space restrictions.
+     */
+    LITE("lite_%s");
+
+    private final String template;
+
+    private Variant(String template) {
+      this.template = template;
+    }
+
+    /**
+     * Returns the basename of the type by adding the name of the current variant. The basename of
+     * a Type is used to determine the name of the source file in which the metadata is defined.
+     *
+     * <p>Note that when the variant is {@link Variant#FULL} this method just returns the type name.
+     */
+    public String getBasename(Type type) {
+      return String.format(template, type);
+    }
+
+    /**
+     * Parses metadata variant name. By default (for a name of {@code ""} or {@code null}) we return
+     * {@link Variant#FULL}, otherwise we match against the variant name (either "test" or "lite").
+     */
+    public static Variant parse(String variantName) {
+      if ("test".equalsIgnoreCase(variantName)) {
+        return Variant.TEST;
+      } else if ("lite".equalsIgnoreCase(variantName)) {
+        return Variant.LITE;
+      } else if (variantName == null || variantName.length() == 0) {
+        return Variant.FULL;
+      } else {
+        return null;
+      }
+    }
+  }
 
   /**
-   * Package private setter used to inject the binary stream for testing purpose.
+   * An immutable options class for parsing and representing the command line options for this
+   * command.
    */
-  void setBinaryStream(ByteArrayOutputStream stream) {
-    this.binaryStream = stream;
+  // @VisibleForTesting
+  static final class Options {
+    private static final Pattern BASENAME_PATTERN =
+        Pattern.compile("(?:(test|lite)_)?([a-z_]+)");
+
+    public static Options parse(String commandName, String[] args) {
+      if (args.length == 4) {
+        String inputXmlFilePath = args[1];
+        String outputDirPath = args[2];
+        Matcher basenameMatcher = BASENAME_PATTERN.matcher(args[3]);
+        if (basenameMatcher.matches()) {
+          Variant variant = Variant.parse(basenameMatcher.group(1));
+          Type type = Type.parse(basenameMatcher.group(2));
+          if (type != null && variant != null) {
+            return new Options(inputXmlFilePath, outputDirPath, type, variant);
+          }
+        }
+      }
+      throw new IllegalArgumentException(String.format(
+          "Usage: %s <inputXmlFile> <outputDir> ( <type> | test_<type> | lite_<type> )\n" +
+          "       where <type> is one of: %s",
+          commandName, Arrays.asList(Type.values())));
+    }
+
+    // File path where the XML input can be found.
+    private final String inputXmlFilePath;
+    // Output directory where the generated files will be saved.
+    private final String outputDirPath;
+    private final Type type;
+    private final Variant variant;
+
+    private Options(String inputXmlFilePath, String outputDirPath, Type type, Variant variant) {
+      this.inputXmlFilePath = inputXmlFilePath;
+      this.outputDirPath = outputDirPath;
+      this.type = type;
+      this.variant = variant;
+    }
+
+    public String getInputFilePath() {
+      return inputXmlFilePath;
+    }
+
+    public String getOutputDir() {
+      return outputDirPath;
+    }
+
+    public Type getType() {
+      return type;
+    }
+
+    public Variant getVariant() {
+      return variant;
+    }
   }
 
   @Override
@@ -69,187 +150,70 @@ public class BuildMetadataCppFromXml extends Command {
   }
 
   /**
-   * Starts the generation of the code. First it checks parameters from command line. Then it opens
-   * all the streams (input and output streams), emits the header and implementation code and
-   * finally closes all the streams.
+   * Generates C++ header and source files to represent the metadata specified by this command's
+   * arguments. The metadata XML file is read and converted to a byte array before being written
+   * into a C++ source file as a static data array.
    *
    * @return  true if the generation succeeded.
    */
   @Override
   public boolean start() {
-    if (!parseCommandLine()) {
-      return false;
-    }
     try {
-      generateBinaryFromXml();
-      openFiles();
-      emitHeader();
-      emitImplementation();
-    } catch (Exception e) {
+      Options opt = Options.parse(getCommandName(), getArgs());
+      byte[] data = loadMetadataBytes(opt.getInputFilePath(), opt.getVariant() == Variant.LITE);
+      CppMetadataGenerator metadata = CppMetadataGenerator.create(opt.getType(), data);
+
+      // TODO: Consider adding checking for correctness of file paths and access.
+      OutputStream headerStream = null;
+      OutputStream sourceStream = null;
+      try {
+        File dir = new File(opt.getOutputDir());
+        headerStream = openHeaderStream(dir, opt.getType());
+        sourceStream = openSourceStream(dir, opt.getType(), opt.getVariant());
+        metadata.outputHeaderFile(new OutputStreamWriter(headerStream, UTF_8));
+        metadata.outputSourceFile(new OutputStreamWriter(sourceStream, UTF_8));
+      } finally {
+        FileUtils.closeFiles(headerStream, sourceStream);
+      }
+      return true;
+    } catch (IOException e) {
+      System.err.println(e.getMessage());
+    } catch (RuntimeException e) {
       System.err.println(e.getMessage());
-      return false;
-    } finally {
-      FileUtils.closeFiles(headerFileOutputStream, implFileOutputStream);
     }
-    return true;
-  }
-
-  private void generateBinaryFromXml() throws Exception {
-    PhoneMetadataCollection collection =
-        BuildMetadataFromXml.buildPhoneMetadataCollection(inputFilePath, liteMetadata);
-    collection.writeTo(binaryStream);
-  }
-
-  /**
-   * Opens the binary file input stream and the two file output streams used to emit header and
-   * implementation code.
-   */
-  private void openFiles() throws IOException {
-    headerFileOutputStream = new FileOutputStream(String.format("%s/metadata.h", outputDir));
-    implFileOutputStream = new FileOutputStream(String.format("%s/%s.cc", outputDir, baseFilename));
+    return false;
   }
 
-  private void emitNamespacesBeginning(PrintWriter pw) {
-    pw.println("namespace i18n {");
-    pw.println("namespace phonenumbers {");
+  /** Loads the metadata XML file and converts its contents to a byte array. */
+  private byte[] loadMetadataBytes(String inputFilePath, boolean liteMetadata) {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    try {
+      writePhoneMetadataCollection(inputFilePath, liteMetadata, out);
+    } catch (Exception e) {
+      // We cannot recover from any exceptions thrown here, so promote them to runtime exceptions.
+      throw new RuntimeException(e);
+    } finally {
+      FileUtils.closeFiles(out);
+    }
+    return out.toByteArray();
   }
 
-  private void emitNamespacesEnd(PrintWriter pw) {
-    pw.println("}  // namespace phonenumbers");
-    pw.println("}  // namespace i18n");
+  // @VisibleForTesting
+  void writePhoneMetadataCollection(
+      String inputFilePath, boolean liteMetadata, OutputStream out) throws IOException, Exception {
+    BuildMetadataFromXml.buildPhoneMetadataCollection(inputFilePath, liteMetadata).writeTo(out);
   }
 
-  /**
-   * Generates the header file containing the two function prototypes in namespace
-   * i18n::phonenumbers.
-   * <pre>
-   *   int metadata_size();
-   *   const void* metadata_get();
-   * </pre>
-   */
-  private void emitHeader() throws IOException {
-    final PrintWriter pw = new PrintWriter(headerFileOutputStream);
-    CopyrightNotice.writeTo(pw, COPYRIGHT_YEAR);
-    final String guardName = "I18N_PHONENUMBERS_METADATA_H_";
-    pw.println("#ifndef " + guardName);
-    pw.println("#define " + guardName);
-
-    pw.println();
-    emitNamespacesBeginning(pw);
-    pw.println();
-
-    pw.println("int metadata_size();");
-    pw.println("const void* metadata_get();");
-    pw.println();
-
-    emitNamespacesEnd(pw);
-    pw.println();
-
-    pw.println("#endif  // " + guardName);
-    pw.close();
+  // @VisibleForTesting
+  OutputStream openHeaderStream(File dir, Type type) throws FileNotFoundException {
+    return new FileOutputStream(new File(dir, type + ".h"));
   }
 
-  /**
-   * The next two methods generate the implementation file (.cc) containing the file data and the
-   * two function implementations:
-   *
-   * <pre>
-   * #include "X.h"
-   *
-   * namespace i18n {
-   * namespace phonenumbers {
-   *
-   * namespace {
-   *   const unsigned char[] data = { .... };
-   * }  // namespace
-   *
-   * const void* metadata_get() {
-   *   return data;
-   * }
-   *
-   * int metadata_size() {
-   *   return sizeof(data) / sizeof(data[0]);
-   * }
-   *
-   * }  // namespace phonenumbers
-   * }  // namespace i18n
-   *
-   * </pre>
-   */
-
-  /**
-   * Emits the C++ code implementation (.cc file) corresponding to the provided XML input file.
-   */
-  private void emitImplementation() throws IOException {
-    final PrintWriter pw = new PrintWriter(implFileOutputStream);
-    CopyrightNotice.writeTo(pw, COPYRIGHT_YEAR);
-    pw.println("#include \"phonenumbers/metadata.h\"");
-    pw.println();
-
-    emitNamespacesBeginning(pw);
-    pw.println();
-
-    pw.println("namespace {");
-    pw.print("static const unsigned char data[] = {");
-    emitStaticArrayCode(pw);
-    pw.println("};");
-    pw.println("}  // namespace");
-
-    pw.println();
-    pw.println("int metadata_size() {");
-    pw.println("  return sizeof(data) / sizeof(data[0]);");
-    pw.println("}");
-
-    pw.println();
-    pw.println("const void* metadata_get() {");
-    pw.println("  return data;");
-    pw.println("}");
-
-    pw.println();
-    emitNamespacesEnd(pw);
-
-    pw.close();
+  // @VisibleForTesting
+  OutputStream openSourceStream(File dir, Type type, Variant variant) throws FileNotFoundException {
+    return new FileOutputStream(new File(dir, variant.getBasename(type) + ".cc"));
   }
 
-  /**
-   * Emits the C++ code corresponding to the provided XML input file into a static byte array.
-   */
-  void emitStaticArrayCode(PrintWriter pw) throws IOException {
-    byte[] buf = binaryStream.toByteArray();
-    pw.print("\n  ");
-
-    for (int i = 0; i < buf.length; i++) {
-      String format = "0x%02X";
-
-      if (i == buf.length - 1) {
-        format += "\n";
-      } else if ((i + 1) % 13 == 0) {  // 13 bytes per line to have lines of 79 characters.
-        format += ",\n  ";
-      } else {
-        format += ", ";
-      }
-      pw.printf(format, buf[i]);
-    }
-    pw.flush();
-    binaryStream.flush();
-    binaryStream.close();
-  }
-
-  private boolean parseCommandLine() {
-    final String[] args = getArgs();
-
-    if (args.length != 4 || !METADATA_TYPES.contains(args[3])) {
-      System.err.println(String.format(
-          "Usage: %s <inputXmlFile> <outputDir> ( metadata | test_metadata | lite_metadata )",
-          getCommandName()));
-      return false;
-    }
-    // args[0] is the name of the command.
-    inputFilePath = args[1];
-    outputDir = args[2];
-    baseFilename = args[3];
-    liteMetadata = baseFilename.equals("lite_metadata");
-
-    return true;
-  }
+  /** The charset in which our source and header files will be written. */
+  private static final Charset UTF_8 = Charset.forName("UTF-8");
 }
diff --git a/tools/java/cpp-build/src/com/google/i18n/phonenumbers/CppMetadataGenerator.java b/tools/java/cpp-build/src/com/google/i18n/phonenumbers/CppMetadataGenerator.java
new file mode 100644 (file)
index 0000000..f1c6b34
--- /dev/null
@@ -0,0 +1,193 @@
+/*
+ *  Copyright (C) 2012 The Libphonenumber Authors
+ *
+ *  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.i18n.phonenumbers;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Locale;
+
+/**
+ * Encapsulation of binary metadata created from XML to be included as static data in C++ source
+ * files.
+ *
+ * @author David Beaumont
+ * @author Philippe Liard
+ */
+public final class CppMetadataGenerator {
+
+  /**
+   * The metadata type represents the known types of metadata and includes additional information
+   * such as the copyright year. It is expected that the generated files will be named after the
+   * {@link #toString} of their type.
+   */
+  public enum Type {
+    /** The basic phone number metadata (expected to be written to metadata.[h/cc]). */
+    METADATA("metadata", 2011),
+    /** The alternate format metadata (expected to be written to alternate_format.[h/cc]). */
+    ALTERNATE_FORMAT("alternate_format", 2012);
+
+    private final String typeName;
+    private final int copyrightYear;
+
+    private Type(String typeName, int copyrightYear) {
+      this.typeName = typeName;
+      this.copyrightYear = copyrightYear;
+    }
+
+    /** Returns the year in which this metadata type was first introduced. */
+    public int getCopyrightYear() {
+      return copyrightYear;
+    }
+
+    /**
+     * Returns the name of this type for use in C++ source/header files. Use this in preference to
+     * using {@link #name}.
+     */
+    @Override public String toString() {
+      return typeName;
+    }
+
+    /**
+     * Parses the type from a string case-insensitively.
+     *
+     * @return the matching Type instance or null if not matched.
+     */
+    public static Type parse(String typeName) {
+      if (Type.METADATA.toString().equalsIgnoreCase(typeName)) {
+        return Type.METADATA;
+      } else if (Type.ALTERNATE_FORMAT.toString().equalsIgnoreCase(typeName)) {
+        return Type.ALTERNATE_FORMAT;
+      } else {
+        return null;
+      }
+    }
+  }
+
+  /**
+   * Creates a metadata instance that can write C++ source and header files to represent this given
+   * byte array as a static unsigned char array. Note that a direct reference to the byte[] is
+   * retained by the newly created CppXmlMetadata instance, so the caller should treat the array as
+   * immutable after making this call.
+   */
+  public static CppMetadataGenerator create(Type type, byte[] data) {
+    return new CppMetadataGenerator(type, data);
+  }
+
+  private final Type type;
+  private final byte[] data;
+  private final String guardName;      // e.g. "I18N_PHONENUMBERS_<TYPE>_H_"
+  private final String headerInclude;  // e.g. "phonenumbers/<type>.h"
+
+  private CppMetadataGenerator(Type type, byte[] data) {
+    this.type = type;
+    this.data = data;
+    this.guardName = createGuardName(type);
+    this.headerInclude = createHeaderInclude(type);
+  }
+
+  /**
+   * Writes the header file for the C++ representation of the metadata to the given writer. Note
+   * that this method does not close the given writer.
+   */
+  public void outputHeaderFile(Writer out) throws IOException {
+    PrintWriter pw = new PrintWriter(out);
+    CopyrightNotice.writeTo(pw, type.getCopyrightYear());
+    pw.println("#ifndef " + guardName);
+    pw.println("#define " + guardName);
+    pw.println();
+    emitNamespaceStart(pw);
+    pw.println();
+    pw.println("int " + type + "_size();");
+    pw.println("const void* " + type + "_get();");
+    pw.println();
+    emitNamespaceEnd(pw);
+    pw.println();
+    pw.println("#endif  // " + guardName);
+    pw.flush();
+  }
+
+  /**
+   * Writes the source file for the C++ representation of the metadata, including a static array
+   * containing the data itself, to the given writer. Note that this method does not close the given
+   * writer.
+   */
+  public void outputSourceFile(Writer out) throws IOException {
+    // TODO: Consider outputting a load method to return the parsed proto directly.
+    PrintWriter pw = new PrintWriter(out);
+    CopyrightNotice.writeTo(pw, type.getCopyrightYear());
+    pw.println("#include \"" + headerInclude + "\"");
+    pw.println();
+    emitNamespaceStart(pw);
+    pw.println();
+    pw.println("namespace {");
+    pw.println("static const unsigned char data[] = {");
+    emitStaticArrayData(pw, data);
+    pw.println("};");
+    pw.println("}  // namespace");
+    pw.println();
+    pw.println("int " + type + "_size() {");
+    pw.println("  return sizeof(data) / sizeof(data[0]);");
+    pw.println("}");
+    pw.println();
+    pw.println("const void* " + type + "_get() {");
+    pw.println("  return data;");
+    pw.println("}");
+    pw.println();
+    emitNamespaceEnd(pw);
+    pw.flush();
+  }
+
+  private static String createGuardName(Type type) {
+    return String.format("I18N_PHONENUMBERS_%s_H_", type.toString().toUpperCase(Locale.ENGLISH));
+  }
+
+  private static String createHeaderInclude(Type type) {
+    return String.format("phonenumbers/%s.h", type);
+  }
+
+  private static void emitNamespaceStart(PrintWriter pw) {
+    pw.println("namespace i18n {");
+    pw.println("namespace phonenumbers {");
+  }
+
+  private static void emitNamespaceEnd(PrintWriter pw) {
+    pw.println("}  // namespace phonenumbers");
+    pw.println("}  // namespace i18n");
+  }
+
+  /** Emits the C++ code corresponding to the binary metadata as a static byte array. */
+  // @VisibleForTesting
+  static void emitStaticArrayData(PrintWriter pw, byte[] data) {
+    String separator = "  ";
+    for (int i = 0; i < data.length; i++) {
+      pw.print(separator);
+      emitHexByte(pw, data[i]);
+      separator = ((i + 1) % 13 == 0) ? ",\n  " : ", ";
+    }
+    pw.println();
+  }
+
+  /** Emits a single byte in the form 0xHH, where H is an upper case hex digit in [0-9A-F]. */
+  private static void emitHexByte(PrintWriter pw, byte v) {
+    pw.print("0x");
+    pw.print(UPPER_HEX[(v & 0xF0) >>> 4]);
+    pw.print(UPPER_HEX[v & 0xF]);
+  }
+
+  private static final char[] UPPER_HEX = "0123456789ABCDEF".toCharArray();
+}
index 2bdd1bd..a73e472 100644 (file)
Binary files a/tools/java/cpp-build/target/cpp-build-1.0-SNAPSHOT-jar-with-dependencies.jar and b/tools/java/cpp-build/target/cpp-build-1.0-SNAPSHOT-jar-with-dependencies.jar differ
index e1c5eed..b1976e8 100644 (file)
 package com.google.i18n.phonenumbers;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.google.i18n.phonenumbers.BuildMetadataCppFromXml.Options;
+import com.google.i18n.phonenumbers.BuildMetadataCppFromXml.Variant;
+import com.google.i18n.phonenumbers.CppMetadataGenerator.Type;
+
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.PrintWriter;
+import java.io.File;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
 
 /**
- * Tests the BuildMetadataCppFromXml implementation to make sure it emits the expected code.
+ * Tests the BuildMetadataCppFromXml implementation to make sure it parses command line options and
+ * generates code correctly.
  */
 public class BuildMetadataCppFromXmlTest {
 
+  // Various repeated test strings and data.
+  private static final String IGNORED = "IGNORED";
+  private static final String OUTPUT_DIR = "output/dir";
+  private static final String INPUT_PATH_XML = "input/path.xml";
+  private static final byte[] TEST_DATA =
+      new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
+  private static final String CPP_TEST_DATA = "0xCA, 0xFE, 0xBA, 0xBE";
+
   @Test
-  public void emitStaticArrayCode() {
-    final int streamSize = 4;
+  public void parseVariant() {
+    assertNull(Variant.parse("xxx"));
+    assertEquals(Variant.FULL, Variant.parse(null));
+    assertEquals(Variant.FULL, Variant.parse(""));
+    assertEquals(Variant.LITE, Variant.parse("lite"));
+    assertEquals(Variant.TEST, Variant.parse("test"));
+    assertEquals(Variant.LITE, Variant.parse("LITE"));
+    assertEquals(Variant.TEST, Variant.parse("Test"));
+  }
 
+  @Test
+  public void parseBadOptions() {
     try {
-      ByteArrayOutputStream stream = new ByteArrayOutputStream(streamSize);
+      BuildMetadataCppFromXml.Options.parse("MyCommand", new String[] { IGNORED });
+      fail("Expected exception not thrown");
+    } catch (IllegalArgumentException e) {
+      assertTrue(e.getMessage().contains("MyCommand"));
+    }
+  }
+
+  @Test
+  public void parseGoodOptions() {
+    Options opt = BuildMetadataCppFromXml.Options.parse("MyCommand",
+        new String[] { IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "test_alternate_format" });
+    assertEquals(Type.ALTERNATE_FORMAT, opt.getType());
+    assertEquals(Variant.TEST, opt.getVariant());
+    assertEquals(INPUT_PATH_XML, opt.getInputFilePath());
+    assertEquals(OUTPUT_DIR, opt.getOutputDir());
+  }
+
+  @Test
+  public void generateMetadata() {
+    String[] args = new String[] {
+        IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "metadata" };
+    // Most of the useful asserts are done in the mock class.
+    MockedCommand command = new MockedCommand(
+        INPUT_PATH_XML, false, OUTPUT_DIR, Type.METADATA, Variant.FULL);
+    command.setArgs(args);
+    command.start();
+    // Sanity check the captured data (asserting implicitly that the mocked methods were called).
+    String headerString = command.capturedHeaderFile();
+    assertTrue(headerString.contains("const void* metadata_get()"));
+    assertTrue(headerString.contains("int metadata_size()"));
+    String sourceString = command.capturedSourceFile();
+    assertTrue(sourceString.contains("const void* metadata_get()"));
+    assertTrue(sourceString.contains("int metadata_size()"));
+    assertTrue(sourceString.contains(CPP_TEST_DATA));
+  }
+
+  @Test
+  public void generateLiteMetadata() {
+    String[] args = new String[] {
+        IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "lite_metadata" };
+    // Most of the useful asserts are done in the mock class.
+    MockedCommand command = new MockedCommand(
+        INPUT_PATH_XML, true, OUTPUT_DIR, Type.METADATA, Variant.LITE);
+    command.setArgs(args);
+    command.start();
+    // Sanity check the captured data (asserting implicitly that the mocked methods were called).
+    String headerString = command.capturedHeaderFile();
+    assertTrue(headerString.contains("const void* metadata_get()"));
+    assertTrue(headerString.contains("int metadata_size()"));
+    String sourceString = command.capturedSourceFile();
+    assertTrue(sourceString.contains("const void* metadata_get()"));
+    assertTrue(sourceString.contains("int metadata_size()"));
+    assertTrue(sourceString.contains(CPP_TEST_DATA));
+  }
 
-      stream.write(0xca);
-      stream.write(0xfe);
-      stream.write(0xba);
-      stream.write(0xbe);
+  @Test
+  public void generateAlternateFormat() {
+    String[] args = new String[] {
+        IGNORED, INPUT_PATH_XML, OUTPUT_DIR, "alternate_format" };
+    // Most of the useful asserts are done in the mock class.
+    MockedCommand command = new MockedCommand(
+        INPUT_PATH_XML, false, OUTPUT_DIR, Type.ALTERNATE_FORMAT, Variant.FULL);
+    command.setArgs(args);
+    command.start();
+    // Sanity check the captured data (asserting implicitly that the mocked methods were called).
+    String headerString = command.capturedHeaderFile();
+    assertTrue(headerString.contains("const void* alternate_format_get()"));
+    assertTrue(headerString.contains("int alternate_format_size()"));
+    String sourceString = command.capturedSourceFile();
+    assertTrue(sourceString.contains("const void* alternate_format_get()"));
+    assertTrue(sourceString.contains("int alternate_format_size()"));
+    assertTrue(sourceString.contains(CPP_TEST_DATA));
+  }
 
-      ByteArrayOutputStream result = new ByteArrayOutputStream(streamSize);
-      PrintWriter printWriter = new PrintWriter(result);
+  /**
+   * Manually mocked subclass of BuildMetadataCppFromXml which overrides all file related behavior
+   * while asserting the validity of any parameters passed to the mocked methods. After starting
+   * this command, the captured header and source file contents can be retrieved for testing.
+   */
+  static class MockedCommand extends BuildMetadataCppFromXml {
+    private static final Charset UTF_8 = Charset.forName("UTF-8");
+    private final String expectedInputFilePath;
+    private final boolean expectedLiteMetadata;
+    private final String expectedOutputDirPath;
+    private final Type expectedType;
+    private final Variant expectedVariant;
+    private final ByteArrayOutputStream headerOut = new ByteArrayOutputStream();
+    private final ByteArrayOutputStream sourceOut = new ByteArrayOutputStream();
 
-      BuildMetadataCppFromXml buildMetadataCppFromXml = new BuildMetadataCppFromXml();
-      buildMetadataCppFromXml.setBinaryStream(stream);
-      buildMetadataCppFromXml.emitStaticArrayCode(printWriter);
+    public MockedCommand(String expectedInputFilePath, boolean expectedLiteMetadata,
+        String expectedOutputDirPath, Type expectedType, Variant expectedVariant) {
 
-      assertEquals("\n  0xCA, 0xFE, 0xBA, 0xBE\n", result.toString());
-    } catch (IOException e) {
-      fail(e.getMessage());
+      this.expectedInputFilePath = expectedInputFilePath;
+      this.expectedLiteMetadata = expectedLiteMetadata;
+      this.expectedOutputDirPath = expectedOutputDirPath;
+      this.expectedType = expectedType;
+      this.expectedVariant = expectedVariant;
+    }
+    @Override void writePhoneMetadataCollection(
+        String inputFilePath, boolean liteMetadata, OutputStream out) throws Exception {
+      assertEquals(expectedInputFilePath, inputFilePath);
+      assertEquals(expectedLiteMetadata, liteMetadata);
+      out.write(TEST_DATA, 0, TEST_DATA.length);
+    }
+    @Override OutputStream openHeaderStream(File dir, Type type) {
+      assertEquals(expectedOutputDirPath, dir.getPath());
+      assertEquals(expectedType, type);
+      return headerOut;
+    }
+    @Override OutputStream openSourceStream(File dir, Type type, Variant variant) {
+      assertEquals(expectedOutputDirPath, dir.getPath());
+      assertEquals(expectedType, type);
+      assertEquals(expectedVariant, variant);
+      return sourceOut;
+    }
+    String capturedHeaderFile() {
+      return new String(headerOut.toByteArray(), UTF_8);
+    }
+    String capturedSourceFile() {
+      return new String(sourceOut.toByteArray(), UTF_8);
     }
   }
 }
diff --git a/tools/java/cpp-build/test/com/google/i18n/phonenumbers/CppMetadataGeneratorTest.java b/tools/java/cpp-build/test/com/google/i18n/phonenumbers/CppMetadataGeneratorTest.java
new file mode 100644 (file)
index 0000000..4cd9bee
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ *  Copyright (C) 2012 The Libphonenumber Authors
+ *
+ *  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.i18n.phonenumbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.i18n.phonenumbers.CppMetadataGenerator.Type;
+
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Tests that the CppXmlMetadata class emits the expected source and header files for metadata.
+ */
+public class CppMetadataGeneratorTest {
+
+  @Test
+  public void emitStaticArrayData() {
+    // 13 bytes per line, so have 16 bytes to test > 1 line (general case).
+    // Use all hex digits in both nibbles to test hex formatting.
+    byte[] data = new byte[] {
+      (byte) 0xF0, (byte) 0xE1, (byte) 0xD2, (byte) 0xC3,
+      (byte) 0xB4, (byte) 0xA5, (byte) 0x96, (byte) 0x87,
+      (byte) 0x78, (byte) 0x69, (byte) 0x5A, (byte) 0x4B,
+      (byte) 0x3C, (byte) 0x2D, (byte) 0x1E, (byte) 0x0F,
+    };
+
+    StringWriter writer = new StringWriter();
+    CppMetadataGenerator.emitStaticArrayData(new PrintWriter(writer), data);
+    assertEquals(
+        "  0xF0, 0xE1, 0xD2, 0xC3, 0xB4, 0xA5, 0x96, 0x87, 0x78, 0x69, 0x5A, 0x4B, 0x3C,\n" +
+        "  0x2D, 0x1E, 0x0F\n",
+        writer.toString());
+  }
+
+  @Test
+  public void outputHeaderFile() throws IOException {
+    byte[] data = new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
+    CppMetadataGenerator metadata = CppMetadataGenerator.create(Type.METADATA, data);
+
+    StringWriter writer = new StringWriter();
+    metadata.outputHeaderFile(writer);
+    Iterator<String> lines = toLines(writer.toString()).iterator();
+    // Sanity check that at least some of the expected lines are present.
+    assertTrue(consumeUntil(" * Copyright (C) 2011 The Libphonenumber Authors", lines));
+    assertTrue(consumeUntil("#ifndef I18N_PHONENUMBERS_METADATA_H_", lines));
+    assertTrue(consumeUntil("#define I18N_PHONENUMBERS_METADATA_H_", lines));
+    assertTrue(consumeUntil("namespace i18n {", lines));
+    assertTrue(consumeUntil("namespace phonenumbers {", lines));
+    assertTrue(consumeUntil("int metadata_size();", lines));
+    assertTrue(consumeUntil("const void* metadata_get();", lines));
+    assertTrue(consumeUntil("#endif  // I18N_PHONENUMBERS_METADATA_H_", lines));
+  }
+
+  @Test
+  public void outputSourceFile() throws IOException {
+    byte[] data = new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
+    CppMetadataGenerator metadata = CppMetadataGenerator.create(Type.ALTERNATE_FORMAT, data);
+
+    StringWriter writer = new StringWriter();
+    metadata.outputSourceFile(writer);
+    Iterator<String> lines = toLines(writer.toString()).iterator();
+    // Sanity check that at least some of the expected lines are present.
+    assertTrue(consumeUntil(" * Copyright (C) 2012 The Libphonenumber Authors", lines));
+    assertTrue(consumeUntil("namespace i18n {", lines));
+    assertTrue(consumeUntil("namespace phonenumbers {", lines));
+    assertTrue(consumeUntil("namespace {", lines));
+    assertTrue(consumeUntil("static const unsigned char data[] = {", lines));
+    assertTrue(consumeUntil("  0xCA, 0xFE, 0xBA, 0xBE", lines));
+    assertTrue(consumeUntil("int alternate_format_size() {", lines));
+    assertTrue(consumeUntil("const void* alternate_format_get() {", lines));
+  }
+
+  /** Converts a string containing newlines into a list of lines. */
+  private static List<String> toLines(String s) throws IOException {
+    BufferedReader reader = new BufferedReader(new StringReader(s));
+    List<String> lines = new ArrayList<String>();
+    for (String line = reader.readLine(); line != null; line = reader.readLine()) {
+      lines.add(line);
+    }
+    return lines;
+  }
+
+  /**
+   * Consumes strings from the given iterator until the expected string is reached (it is also
+   * consumed). If the expected string is not found, the iterator is exhausted and {@code false} is
+   * returned.
+   *
+   * @return true if the expected string was found while consuming the iterator.
+   */
+  private static boolean consumeUntil(String expected, Iterator<String> it) {
+    while (it.hasNext()) {
+      if (it.next().equals(expected)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
index 2d63a11..0416952 100644 (file)
Binary files a/tools/java/java-build/target/java-build-1.0-SNAPSHOT-jar-with-dependencies.jar and b/tools/java/java-build/target/java-build-1.0-SNAPSHOT-jar-with-dependencies.jar differ