diff options
author | dbeaumont@google.com <dbeaumont@google.com@ee073f10-1060-11df-b6a4-87a95322a99c> | 2012-07-20 14:21:29 +0000 |
---|---|---|
committer | dbeaumont@google.com <dbeaumont@google.com@ee073f10-1060-11df-b6a4-87a95322a99c> | 2012-07-20 14:21:29 +0000 |
commit | 36a2c8aea21847e53f97dff6a4f545aa66cbe92c (patch) | |
tree | f9a577cb793c81e85ffddf37316dd4a69a7ce4d6 | |
parent | d89f3ed7baf1383842e666892e2066200ca06dca (diff) | |
download | android_external_libphonenumbergoogle-36a2c8aea21847e53f97dff6a4f545aa66cbe92c.tar.gz android_external_libphonenumbergoogle-36a2c8aea21847e53f97dff6a4f545aa66cbe92c.tar.bz2 android_external_libphonenumbergoogle-36a2c8aea21847e53f97dff6a4f545aa66cbe92c.zip |
changes to allow for generation of alternate format metadata
git-svn-id: http://libphonenumber.googlecode.com/svn/trunk@507 ee073f10-1060-11df-b6a4-87a95322a99c
8 files changed, 673 insertions, 268 deletions
diff --git a/tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java b/tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java index a5fb1ca..67298ac 100644 --- a/tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java +++ b/tools/java/common/src/com/google/i18n/phonenumbers/BuildMetadataFromXml.java @@ -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(); } } diff --git a/tools/java/common/test/com/google/i18n/phonenumbers/BuildMetadataFromXmlTest.java b/tools/java/common/test/com/google/i18n/phonenumbers/BuildMetadataFromXmlTest.java index f46da32..bed22b7 100644 --- a/tools/java/common/test/com/google/i18n/phonenumbers/BuildMetadataFromXmlTest.java +++ b/tools/java/common/test/com/google/i18n/phonenumbers/BuildMetadataFromXmlTest.java @@ -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()); diff --git a/tools/java/cpp-build/src/com/google/i18n/phonenumbers/BuildMetadataCppFromXml.java b/tools/java/cpp-build/src/com/google/i18n/phonenumbers/BuildMetadataCppFromXml.java index ddc01e3..baf3422 100644 --- a/tools/java/cpp-build/src/com/google/i18n/phonenumbers/BuildMetadataCppFromXml.java +++ b/tools/java/cpp-build/src/com/google/i18n/phonenumbers/BuildMetadataCppFromXml.java @@ -16,15 +16,19 @@ 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 index 0000000..f1c6b34 --- /dev/null +++ b/tools/java/cpp-build/src/com/google/i18n/phonenumbers/CppMetadataGenerator.java @@ -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(); +} diff --git a/tools/java/cpp-build/target/cpp-build-1.0-SNAPSHOT-jar-with-dependencies.jar b/tools/java/cpp-build/target/cpp-build-1.0-SNAPSHOT-jar-with-dependencies.jar Binary files differindex 2bdd1bd..a73e472 100644 --- a/tools/java/cpp-build/target/cpp-build-1.0-SNAPSHOT-jar-with-dependencies.jar +++ b/tools/java/cpp-build/target/cpp-build-1.0-SNAPSHOT-jar-with-dependencies.jar diff --git a/tools/java/cpp-build/test/com/google/i18n/phonenumbers/BuildMetadataCppFromXmlTest.java b/tools/java/cpp-build/test/com/google/i18n/phonenumbers/BuildMetadataCppFromXmlTest.java index e1c5eed..b1976e8 100644 --- a/tools/java/cpp-build/test/com/google/i18n/phonenumbers/BuildMetadataCppFromXmlTest.java +++ b/tools/java/cpp-build/test/com/google/i18n/phonenumbers/BuildMetadataCppFromXmlTest.java @@ -17,41 +17,169 @@ 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 index 0000000..4cd9bee --- /dev/null +++ b/tools/java/cpp-build/test/com/google/i18n/phonenumbers/CppMetadataGeneratorTest.java @@ -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; + } +} diff --git a/tools/java/java-build/target/java-build-1.0-SNAPSHOT-jar-with-dependencies.jar b/tools/java/java-build/target/java-build-1.0-SNAPSHOT-jar-with-dependencies.jar Binary files differindex 2d63a11..0416952 100644 --- a/tools/java/java-build/target/java-build-1.0-SNAPSHOT-jar-with-dependencies.jar +++ b/tools/java/java-build/target/java-build-1.0-SNAPSHOT-jar-with-dependencies.jar |