TOOLS: Adding metadata code generation to C++ build tools. Patch contributed by phili...
authorjia.shao.peng <jia.shao.peng@ee073f10-1060-11df-b6a4-87a95322a99c>
Wed, 27 Apr 2011 13:32:25 +0000 (13:32 +0000)
committerjia.shao.peng <jia.shao.peng@ee073f10-1060-11df-b6a4-87a95322a99c>
Wed, 27 Apr 2011 13:32:25 +0000 (13:32 +0000)
git-svn-id: http://libphonenumber.googlecode.com/svn/trunk@185 ee073f10-1060-11df-b6a4-87a95322a99c

tools/java/common/pom.xml [deleted file]
tools/java/common/src/com/google/i18n/phonenumbers/tools/BuildMetadataFromXml.java [new file with mode: 0644]
tools/java/common/src/com/google/i18n/phonenumbers/tools/CopyrightNotice.java [new file with mode: 0644]
tools/java/cpp-build/pom.xml
tools/java/cpp-build/src/com/google/i18n/phonenumbers/tools/BuildMetadataCppFromXml.java [new file with mode: 0644]
tools/java/cpp-build/src/com/google/i18n/phonenumbers/tools/EntryPoint.java [new file with mode: 0644]
tools/java/cpp-build/test/com/google/i18n/phonenumbers/tools/BuildMetadataCppFromXmlTest.java [new file with mode: 0644]
tools/java/pom.xml

diff --git a/tools/java/common/pom.xml b/tools/java/common/pom.xml
deleted file mode 100644 (file)
index 14a8962..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0"?>
-<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <artifactId>tools</artifactId>
-    <groupId>com.google.i18n.phonenumbers</groupId>
-    <version>1.0-SNAPSHOT</version>
-  </parent>
-
-  <groupId>com.google.i18n.phonenumbers.tools</groupId>
-  <artifactId>common</artifactId>
-  <version>1.0-SNAPSHOT</version>
-  <name>Libphonenumber common library for build tools</name>
-  <description>
-    This library contains helper classes designed to ease file manipulation and command dispatching
-    which is required by build tools dealing with code generation and multiple commands invocation
-    from a single entry point.
-  </description>
-
-  <build>
-    <sourceDirectory>src</sourceDirectory>
-    <plugins>
-      <plugin>
-        <version>2.3.2</version>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-compiler-plugin</artifactId>
-        <configuration>
-          <source>1.5</source>
-          <target>1.5</target>
-        </configuration>
-      </plugin>
-    </plugins>
-  </build>
-
-</project>
diff --git a/tools/java/common/src/com/google/i18n/phonenumbers/tools/BuildMetadataFromXml.java b/tools/java/common/src/com/google/i18n/phonenumbers/tools/BuildMetadataFromXml.java
new file mode 100644 (file)
index 0000000..f3ad60a
--- /dev/null
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2009 Google Inc.
+ *
+ * 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.tools;
+
+import com.google.i18n.phonenumbers.Phonemetadata.NumberFormat;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadata;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneNumberDesc;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+/**
+ * Library to build phone number metadata from the XML format.
+ *
+ * @author Shaopeng Jia
+ */
+public class BuildMetadataFromXml {
+  private static final Logger LOGGER = Logger.getLogger(BuildMetadataFromXml.class.getName());
+  private static Boolean liteBuild;
+
+  // 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);
+    Document document = builder.parse(xmlFile);
+    document.getDocumentElement().normalize();
+    Element rootElement = document.getDocumentElement();
+    NodeList territory = rootElement.getElementsByTagName("territory");
+    PhoneMetadataCollection.Builder metadataCollection = PhoneMetadataCollection.newBuilder();
+    int numOfTerritories = territory.getLength();
+    for (int i = 0; i < numOfTerritories; i++) {
+      Element territoryElement = (Element) territory.item(i);
+      String regionCode = territoryElement.getAttribute("id");
+      PhoneMetadata metadata = loadCountryMetadata(regionCode, territoryElement);
+      metadataCollection.addMetadata(metadata);
+    }
+    return metadataCollection.build();
+  }
+
+  // Build a mapping from a country calling code to the region codes which denote the country/region
+  // represented by that country code. In the case of multiple countries sharing a calling code,
+  // such as the NANPA countries, the one indicated with "isMainCountryForCode" in the metadata
+  // should be first.
+  public static Map<Integer, List<String>> buildCountryCodeToRegionCodeMap(
+      PhoneMetadataCollection metadataCollection) {
+    Map<Integer, List<String>> countryCodeToRegionCodeMap =
+        new TreeMap<Integer, List<String>>();
+    for (PhoneMetadata metadata : metadataCollection.getMetadataList()) {
+      String regionCode = metadata.getId();
+      int countryCode = metadata.getCountryCode();
+      if (countryCodeToRegionCodeMap.containsKey(countryCode)) {
+        if (metadata.getMainCountryForCode()) {
+          countryCodeToRegionCodeMap.get(countryCode).add(0, regionCode);
+        } else {
+          countryCodeToRegionCodeMap.get(countryCode).add(regionCode);
+        }
+      } else {
+        // For most countries, there will be only one region code for the country calling code.
+        List<String> listWithRegionCode = new ArrayList<String>(1);
+        listWithRegionCode.add(regionCode);
+        countryCodeToRegionCodeMap.put(countryCode, listWithRegionCode);
+      }
+    }
+    return countryCodeToRegionCodeMap;
+  }
+
+  private static String validateRE(String regex) {
+    return validateRE(regex, false);
+  }
+
+  private static String validateRE(String regex, boolean removeWhitespace) {
+    // Removes all the whitespace and newline from the regexp. Not using pattern compile options to
+    // make it work across programming languages.
+    if (removeWhitespace) {
+      regex = regex.replaceAll("\\s", "");
+    }
+    Pattern.compile(regex);
+    // return regex itself if it is of correct regex syntax
+    // i.e. compile did not fail with a PatternSyntaxException.
+    return regex;
+  }
+
+  private static PhoneMetadata loadCountryMetadata(String regionCode, Element element) {
+    PhoneMetadata.Builder metadata = PhoneMetadata.newBuilder();
+    metadata.setId(regionCode);
+    metadata.setCountryCode(Integer.parseInt(element.getAttribute("countryCode")));
+    if (element.hasAttribute("leadingDigits")) {
+      metadata.setLeadingDigits(validateRE(element.getAttribute("leadingDigits")));
+    }
+    metadata.setInternationalPrefix(validateRE(element.getAttribute("internationalPrefix")));
+    if (element.hasAttribute("preferredInternationalPrefix")) {
+      String preferredInternationalPrefix = element.getAttribute("preferredInternationalPrefix");
+      metadata.setPreferredInternationalPrefix(preferredInternationalPrefix);
+    }
+    if (element.hasAttribute("nationalPrefixForParsing")) {
+      metadata.setNationalPrefixForParsing(
+          validateRE(element.getAttribute("nationalPrefixForParsing")));
+      if (element.hasAttribute("nationalPrefixTransformRule")) {
+        metadata.setNationalPrefixTransformRule(
+            validateRE(element.getAttribute("nationalPrefixTransformRule")));
+      }
+    }
+    String nationalPrefix = "";
+    String nationalPrefixFormattingRule = "";
+    if (element.hasAttribute("nationalPrefix")) {
+      nationalPrefix = element.getAttribute("nationalPrefix");
+      metadata.setNationalPrefix(nationalPrefix);
+      nationalPrefixFormattingRule =
+          getNationalPrefixFormattingRuleFromElement(element, nationalPrefix);
+
+      if (!metadata.hasNationalPrefixForParsing()) {
+        metadata.setNationalPrefixForParsing(nationalPrefix);
+      }
+    }
+    String carrierCodeFormattingRule = "";
+    if (element.hasAttribute("carrierCodeFormattingRule")) {
+      carrierCodeFormattingRule = validateRE(
+          getDomesticCarrierCodeFormattingRuleFromElement(element, nationalPrefix));
+    }
+    if (element.hasAttribute("preferredExtnPrefix")) {
+      metadata.setPreferredExtnPrefix(element.getAttribute("preferredExtnPrefix"));
+    }
+    if (element.hasAttribute("mainCountryForCode")) {
+      metadata.setMainCountryForCode(true);
+    }
+    if (element.hasAttribute("leadingZeroPossible")) {
+      metadata.setLeadingZeroPossible(true);
+    }
+
+    // Extract availableFormats
+    NodeList numberFormatElements = element.getElementsByTagName("numberFormat");
+    int numOfFormatElements = numberFormatElements.getLength();
+    if (numOfFormatElements > 0) {
+      for (int i = 0; i < numOfFormatElements; i++) {
+        Element numberFormatElement = (Element) numberFormatElements.item(i);
+        NumberFormat.Builder format = NumberFormat.newBuilder();
+        if (numberFormatElement.hasAttribute("nationalPrefixFormattingRule")) {
+          format.setNationalPrefixFormattingRule(
+              getNationalPrefixFormattingRuleFromElement(numberFormatElement, nationalPrefix));
+        } else {
+          format.setNationalPrefixFormattingRule(nationalPrefixFormattingRule);
+        }
+        if (numberFormatElement.hasAttribute("carrierCodeFormattingRule")) {
+          format.setDomesticCarrierCodeFormattingRule(validateRE(
+              getDomesticCarrierCodeFormattingRuleFromElement(numberFormatElement,
+                                                              nationalPrefix)));
+        } else {
+          format.setDomesticCarrierCodeFormattingRule(carrierCodeFormattingRule);
+        }
+        setLeadingDigitsPatterns(numberFormatElement, format);
+        format.setPattern(validateRE(numberFormatElement.getAttribute("pattern")));
+        NodeList formatPattern = numberFormatElement.getElementsByTagName("format");
+        if (formatPattern.getLength() != 1) {
+          LOGGER.log(Level.SEVERE,
+                     "Only one format pattern for a numberFormat element should be defined.");
+          throw new RuntimeException("Invalid number of format patterns for country: " +
+                                     regionCode);
+        }
+        format.setFormat(formatPattern.item(0).getFirstChild().getNodeValue());
+        metadata.addNumberFormat(format);
+      }
+    }
+
+    NodeList intlNumberFormatElements = element.getElementsByTagName("intlNumberFormat");
+    int numOfIntlFormatElements = intlNumberFormatElements.getLength();
+    if (numOfIntlFormatElements > 0) {
+      for (int i = 0; i < numOfIntlFormatElements; i++) {
+        Element numberFormatElement = (Element) intlNumberFormatElements.item(i);
+        NumberFormat.Builder format = NumberFormat.newBuilder();
+        setLeadingDigitsPatterns(numberFormatElement, format);
+        format.setPattern(validateRE(numberFormatElement.getAttribute("pattern")));
+        NodeList formatPattern = numberFormatElement.getElementsByTagName("format");
+        if (formatPattern.getLength() != 1) {
+          LOGGER.log(Level.SEVERE,
+                     "Only one format pattern for a numberFormat element should be defined.");
+          throw new RuntimeException("Invalid number of format patterns for country: " +
+                                     regionCode);
+        }
+        format.setFormat(validateRE(formatPattern.item(0).getFirstChild().getNodeValue()));
+        if (numberFormatElement.hasAttribute("carrierCodeFormattingRule")) {
+          format.setDomesticCarrierCodeFormattingRule(validateRE(
+              getDomesticCarrierCodeFormattingRuleFromElement(numberFormatElement,
+                                                              nationalPrefix)));
+        } else {
+          format.setDomesticCarrierCodeFormattingRule(carrierCodeFormattingRule);
+        }
+        metadata.addIntlNumberFormat(format);
+      }
+    }
+
+    PhoneNumberDesc.Builder generalDesc = PhoneNumberDesc.newBuilder();
+    generalDesc = processPhoneNumberDescElement(generalDesc, element, "generalDesc");
+    metadata.setGeneralDesc(generalDesc);
+    metadata.setFixedLine(processPhoneNumberDescElement(generalDesc, element, "fixedLine"));
+    metadata.setMobile(processPhoneNumberDescElement(generalDesc, element, "mobile"));
+    metadata.setTollFree(processPhoneNumberDescElement(generalDesc, element, "tollFree"));
+    metadata.setPremiumRate(processPhoneNumberDescElement(generalDesc, element, "premiumRate"));
+    metadata.setSharedCost(processPhoneNumberDescElement(generalDesc, element, "sharedCost"));
+    metadata.setVoip(processPhoneNumberDescElement(generalDesc, element, "voip"));
+    metadata.setPersonalNumber(processPhoneNumberDescElement(generalDesc, element,
+                                                             "personalNumber"));
+    metadata.setPager(processPhoneNumberDescElement(generalDesc, element, "pager"));
+    metadata.setUan(processPhoneNumberDescElement(generalDesc, element, "uan"));
+    metadata.setNoInternationalDialling(processPhoneNumberDescElement(generalDesc, element,
+                                                                      "noInternationalDialling"));
+
+    if (metadata.getMobile().getNationalNumberPattern().equals(
+        metadata.getFixedLine().getNationalNumberPattern())) {
+      metadata.setSameMobileAndFixedLinePattern(true);
+    }
+    return metadata.build();
+  }
+
+  private static void setLeadingDigitsPatterns(Element numberFormatElement,
+                                               NumberFormat.Builder format) {
+    NodeList leadingDigitsPatternNodes = numberFormatElement.getElementsByTagName("leadingDigits");
+    int numOfLeadingDigitsPatterns = leadingDigitsPatternNodes.getLength();
+    if (numOfLeadingDigitsPatterns > 0) {
+      for (int i = 0; i < numOfLeadingDigitsPatterns; i++) {
+        format.addLeadingDigitsPattern(
+            validateRE((leadingDigitsPatternNodes.item(i)).getFirstChild().getNodeValue(), true));
+      }
+    }
+  }
+
+  private static String getNationalPrefixFormattingRuleFromElement(Element element,
+                                                                   String nationalPrefix) {
+    String nationalPrefixFormattingRule = element.getAttribute("nationalPrefixFormattingRule");
+    // Replace $NP with national prefix and $FG with the first group ($1).
+    nationalPrefixFormattingRule =
+        nationalPrefixFormattingRule.replaceFirst("\\$NP", nationalPrefix)
+            .replaceFirst("\\$FG", "\\$1");
+    return nationalPrefixFormattingRule;
+  }
+
+  private static String getDomesticCarrierCodeFormattingRuleFromElement(Element element,
+                                                                        String nationalPrefix) {
+    String carrierCodeFormattingRule = element.getAttribute("carrierCodeFormattingRule");
+    // Replace $FG with the first group ($1) and $NP with the national prefix.
+    carrierCodeFormattingRule = carrierCodeFormattingRule.replaceFirst("\\$FG", "\\$1")
+        .replaceFirst("\\$NP", nationalPrefix);
+    return carrierCodeFormattingRule;
+  }
+
+  /**
+   * Processes a phone number description element from the XML file and returns it as a
+   * PhoneNumberDesc. If the description element is a fixed line or mobile number, the general
+   * description will be used to fill in the whole element if necessary, or any components that are
+   * missing. For all other types, the general description will only be used to fill in missing
+   * components if the type has a partial definition. For example, if no "tollFree" element exists,
+   * we assume there are no toll free numbers for that locale, and return a phone number description
+   * with "NA" for both the national and possible number patterns.
+   *
+   * @param generalDesc  a generic phone number description that will be used to fill in missing
+   *                     parts of the description
+   * @param countryElement  the XML element representing all the country information
+   * @param numberType  the name of the number type, corresponding to the appropriate tag in the XML
+   *                    file with information about that type
+   * @return  complete description of that phone number type
+   */
+  private static PhoneNumberDesc.Builder processPhoneNumberDescElement(
+          PhoneNumberDesc.Builder generalDesc,
+          Element countryElement,
+          String numberType) {
+    NodeList phoneNumberDescList = countryElement.getElementsByTagName(numberType);
+    PhoneNumberDesc.Builder numberDesc = PhoneNumberDesc.newBuilder();
+    if (phoneNumberDescList.getLength() == 0 &&
+        (!numberType.equals("fixedLine") && !numberType.equals("mobile") &&
+         !numberType.equals("generalDesc"))) {
+      numberDesc.setNationalNumberPattern("NA");
+      numberDesc.setPossibleNumberPattern("NA");
+      return numberDesc;
+    }
+    numberDesc.mergeFrom(generalDesc.build());
+    if (phoneNumberDescList.getLength() > 0) {
+      Element element = (Element) phoneNumberDescList.item(0);
+      NodeList possiblePattern = element.getElementsByTagName("possibleNumberPattern");
+      if (possiblePattern.getLength() > 0) {
+        numberDesc.setPossibleNumberPattern(
+            validateRE(possiblePattern.item(0).getFirstChild().getNodeValue(), true));
+      }
+
+      NodeList validPattern = element.getElementsByTagName("nationalNumberPattern");
+      if (validPattern.getLength() > 0) {
+        numberDesc.setNationalNumberPattern(
+            validateRE(validPattern.item(0).getFirstChild().getNodeValue(), true));
+      }
+
+      if (!liteBuild) {
+        NodeList exampleNumber = element.getElementsByTagName("exampleNumber");
+        if (exampleNumber.getLength() > 0) {
+          numberDesc.setExampleNumber(exampleNumber.item(0).getFirstChild().getNodeValue());
+        }
+      }
+    }
+    return numberDesc;
+  }
+}
diff --git a/tools/java/common/src/com/google/i18n/phonenumbers/tools/CopyrightNotice.java b/tools/java/common/src/com/google/i18n/phonenumbers/tools/CopyrightNotice.java
new file mode 100644 (file)
index 0000000..7efd7d1
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ *  Copyright (C) 2011 Google Inc.
+ *
+ *  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.tools;
+
+import java.util.Calendar;
+
+/**
+ * Class containing the Apache copyright notice used by code generators.
+ *
+ * @author Philippe Liard
+ */
+public class CopyrightNotice {
+
+  public static final String TEXT =
+    "/*\n" +
+    " *  Copyright (C) " + Calendar.getInstance().get(Calendar.YEAR) + " Google Inc.\n" +
+    " *\n" +
+    " *  Licensed under the Apache License, Version 2.0 (the \"License\");\n" +
+    " *  you may not use this file except in compliance with the License.\n" +
+    " *  You may obtain a copy of the License at\n" +
+    " *\n" +
+    " *  http://www.apache.org/licenses/LICENSE-2.0\n" +
+    " *\n" +
+    " *  Unless required by applicable law or agreed to in writing, software\n" +
+    " *  distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
+    " *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
+    " *  See the License for the specific language governing permissions and\n" +
+    " *  limitations under the License.\n" +
+    " */\n";
+}
index efb08c0..9434853 100644 (file)
@@ -20,6 +20,7 @@
 
   <build>
     <sourceDirectory>src</sourceDirectory>
+    <testSourceDirectory>test</testSourceDirectory>
     <plugins>
       <plugin>
         <version>2.3.2</version>
@@ -64,6 +65,8 @@
             <configuration>
               <sources>
                 <source>generated/</source>
+                <!-- Also add ../common/src/ which contains BuildMetadataFromXml.java -->
+                <source>../common/src/</source>
               </sources>
             </configuration>
           </execution>
 
   <dependencies>
     <dependency>
-      <groupId>com.google.i18n.phonenumbers.tools</groupId>
-      <artifactId>common</artifactId>
-      <version>1.0-SNAPSHOT</version>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.8.1</version>
     </dependency>
     <dependency>
       <groupId>com.google.protobuf</groupId>
diff --git a/tools/java/cpp-build/src/com/google/i18n/phonenumbers/tools/BuildMetadataCppFromXml.java b/tools/java/cpp-build/src/com/google/i18n/phonenumbers/tools/BuildMetadataCppFromXml.java
new file mode 100644 (file)
index 0000000..6c5bb21
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ *  Copyright (C) 2011 Google Inc.
+ *
+ *  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.tools;
+
+import com.google.i18n.phonenumbers.Phonemetadata.PhoneMetadataCollection;
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * This class generates the C++ code representation of the provided XML metadata file. It lets us
+ * embed metadata directly in a native binary. We link the object resulting from the compilation of
+ * the code emitted by this class with the C++ phonenumber library.
+ *
+ * @author Philippe Liard
+ */
+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;
+  // Either test_metadata or metadata.{cc,hh} depending on the value of the 'forTesting' command
+  // line parameter.
+  private String baseFilename;
+
+  // 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;
+
+  /**
+   * Package private setter used to inject the binary stream for testing purpose.
+   */
+  void setBinaryStream(ByteArrayOutputStream stream) {
+    this.binaryStream = stream;
+  }
+
+  @Override
+  public String getCommandName() {
+    return "BuildMetadataCppFromXml";
+  }
+
+  /**
+   * 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.
+   *
+   * @return  true if the generation succeeded.
+   */
+  @Override
+  public boolean start() {
+    if (!parseCommandLine()) {
+      return false;
+    }
+    try {
+      generateBinaryFromXml();
+      openFiles();
+      emitHeader();
+      emitImplementation();
+    } catch (Exception 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, false);
+    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/%s.h", outputDir, baseFilename));
+    implFileOutputStream = new FileOutputStream(String.format("%s/%s.cc", outputDir, baseFilename));
+  }
+
+  /**
+   * Generates the header file containing the two function prototypes:
+   * <pre>
+   *   int X_size();
+   *   const void* X_get();
+   * </pre>
+   *
+   * with X: 'metadata' or 'test_metadata'.
+   */
+  private void emitHeader() {
+    final PrintWriter pw = new PrintWriter(headerFileOutputStream);
+    pw.write(CopyrightNotice.TEXT);
+    final String guardName = String.format("EMBEDDED_DATA_%s_H_", baseFilename.toUpperCase());
+    pw.println("#ifndef " + guardName);
+    pw.println("#define " + guardName);
+
+    pw.println();
+    pw.println(String.format("int %s_size();", baseFilename));
+    pw.println(String.format("const void* %s_get();", baseFilename));
+    pw.println();
+
+    pw.println("#endif  // " + guardName);
+    pw.close();
+  }
+
+  /**
+   * The next two methods generate the implementation file (.cc) containing the file data and the
+   * two function implementations:
+   *
+   * <pre>
+   * #include "X.h"
+   *
+   * static const unsigned char[] X_data = { .... };
+   *
+   * const void* X_get() {
+   *   return X_data;
+   * }
+   *
+   * unsigned int X_size() {
+   *   return sizeof(X_data) / sizeof(X_data[0]);
+   * }
+   * </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);
+    pw.write(CopyrightNotice.TEXT);
+    pw.println(String.format("#include \"%s.h\"", baseFilename));
+    pw.println();
+    pw.print(String.format("static const unsigned char %s_data[] = { ", baseFilename));
+    emitStaticArrayCode(pw);
+    pw.println(" };");
+
+    pw.println();
+    pw.println(String.format("int %s_size() {", baseFilename));
+    pw.println(String.format("  return sizeof(%s_data) / sizeof(%s_data[0]);",
+                             baseFilename, baseFilename));
+    pw.println("}");
+
+    pw.println();
+    pw.println(String.format("const void* %s_get() {", baseFilename));
+    pw.println(String.format("  return %s_data;", baseFilename));
+    pw.println("}");
+    pw.close();
+  }
+
+  /**
+   * 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();
+
+    for (int i = 0; i < buf.length; i++) {
+      pw.printf("0x%02X, ", buf[i]);
+    }
+    pw.flush();
+    binaryStream.flush();
+    binaryStream.close();
+  }
+
+  private boolean parseCommandLine() {
+    final String[] args = getArgs();
+
+    if (args.length != 4) {
+      System.err.println(String.format("Usage: %s <inputXmlFile> <outputDir> <forTesting>",
+                                       getCommandName()));
+      return false;
+    }
+    // args[0] is the name of the command.
+    inputFilePath = args[1];
+    outputDir = args[2];
+    baseFilename = Boolean.parseBoolean(args[3]) ? "test_metadata" : "metadata";
+
+    return true;
+  }
+}
diff --git a/tools/java/cpp-build/src/com/google/i18n/phonenumbers/tools/EntryPoint.java b/tools/java/cpp-build/src/com/google/i18n/phonenumbers/tools/EntryPoint.java
new file mode 100644 (file)
index 0000000..47e4012
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ *  Copyright (C) 2011 Google Inc.
+ *
+ *  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.tools;
+
+/**
+ * Entry point class for C++ build tools.
+ *
+ * @author Philippe Liard
+ */
+public class EntryPoint {
+
+  public static void main(String[] args) {
+    boolean status = new CommandDispatcher(args, new Command[] {
+      new BuildMetadataCppFromXml()
+    }).start();
+
+    System.exit(status ? 0 : 1);
+  }
+}
diff --git a/tools/java/cpp-build/test/com/google/i18n/phonenumbers/tools/BuildMetadataCppFromXmlTest.java b/tools/java/cpp-build/test/com/google/i18n/phonenumbers/tools/BuildMetadataCppFromXmlTest.java
new file mode 100644 (file)
index 0000000..658b390
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ *  Copyright (C) 2011 Google Inc.
+ *
+ *  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.tools;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * Tests the BuildMetadataCppFromXml implementation to make sure it emits the expected code.
+ */
+public class BuildMetadataCppFromXmlTest {
+
+  @Test
+  public void emitStaticArrayCode() {
+    final int streamSize = 4;
+
+    try {
+      ByteArrayOutputStream stream = new ByteArrayOutputStream(streamSize);
+
+      stream.write(0xca);
+      stream.write(0xfe);
+      stream.write(0xba);
+      stream.write(0xbe);
+
+      ByteArrayOutputStream result = new ByteArrayOutputStream(streamSize);
+      PrintWriter printWriter = new PrintWriter(result);
+
+      BuildMetadataCppFromXml buildMetadataCppFromXml = new BuildMetadataCppFromXml();
+      buildMetadataCppFromXml.setBinaryStream(stream);
+      buildMetadataCppFromXml.emitStaticArrayCode(printWriter);
+
+      assertEquals("0xCA, 0xFE, 0xBA, 0xBE, ", result.toString());
+    } catch (IOException e) {
+      fail(e.getMessage());
+    }
+  }
+}
index 0fb4b82..efd7b7d 100644 (file)
@@ -20,7 +20,7 @@
   </licenses>
 
   <modules>
-    <module>common</module>
+    <module>cpp-build</module>
   </modules>
 
 </project>